From ad6912557c7d842a380f58ae824c1b4581805280 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20S=C4=99k?= Date: Mon, 29 Sep 2025 13:01:25 +0200 Subject: [PATCH 01/18] fix: no-any :madge --- .../audiodocs/docs/worklets/worklet-node.mdx | 2 +- .../RecordPlayerExample/VinylAnimation.tsx | 42 ++++++++++--------- .../src/core/BaseAudioContext.ts | 22 +++++----- .../react-native-audio-api/src/interfaces.ts | 19 +++++---- .../react-native-audio-api/src/utils/index.ts | 10 ++++- 5 files changed, 55 insertions(+), 40 deletions(-) diff --git a/packages/audiodocs/docs/worklets/worklet-node.mdx b/packages/audiodocs/docs/worklets/worklet-node.mdx index 023edccf3..d54124209 100644 --- a/packages/audiodocs/docs/worklets/worklet-node.mdx +++ b/packages/audiodocs/docs/worklets/worklet-node.mdx @@ -27,7 +27,7 @@ function App() { }); const audioContext = new AudioContext({ sampleRate: 16000 }); - const worklet = (audioData: Array, inputChannelCount: number) => { + const worklet = (audioData: Array, inputChannelCount: number) => { 'worklet'; // here you have access to the number of input channels and the audio data // audio data is a two dimensional array where first index is the channel number and second is buffer of exactly bufferLength size diff --git a/packages/audiodocs/src/components/RecordPlayerExample/VinylAnimation.tsx b/packages/audiodocs/src/components/RecordPlayerExample/VinylAnimation.tsx index 6e5ae1ede..4c5f93323 100644 --- a/packages/audiodocs/src/components/RecordPlayerExample/VinylAnimation.tsx +++ b/packages/audiodocs/src/components/RecordPlayerExample/VinylAnimation.tsx @@ -1,17 +1,18 @@ -import React, { useRef, useEffect, useMemo, useState } from 'react'; +import MockAudioContext from '@site/src/audio/MockAudioContext'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; import * as THREE from 'three'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; import { RoundedBoxGeometry } from 'three/examples/jsm/geometries/RoundedBoxGeometry.js'; +import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial.js'; import { LineSegments2 } from 'three/examples/jsm/lines/LineSegments2.js'; import { LineSegmentsGeometry } from 'three/examples/jsm/lines/LineSegmentsGeometry.js'; -import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial.js'; // @ts-ignore import labelImage from '/static/img/logo.png'; // @ts-ignore -import swmLogo from '/static/img/swm-text.png'; -import styles from './styles.module.css'; import { VINYL_CONSTANTS as C } from './consts'; +import styles from './styles.module.css'; +import swmLogo from '/static/img/swm-text.png'; interface SceneRefs { recordGroup: THREE.Group; @@ -31,7 +32,7 @@ export default function VinylPlayer(): React.ReactElement { const mountRef = useRef(null); const inputRef = useRef(null); - // Animation state ref + // Animation state ref const animationStateRef = useRef({ isSliderPressed: false, targetCoverY: 0, @@ -40,11 +41,12 @@ export default function VinylPlayer(): React.ReactElement { fallVelocity: 0, }); - // Scene object refs + // Scene object refs const sceneRefsRef = useRef(null); const { context, filter, gain } = useMemo(() => { - const ctx = new AudioContext(); + const ctx = typeof window !== 'undefined' ? new AudioContext() : MockAudioContext; + const filterNode = ctx.createBiquadFilter(); filterNode.type = 'lowpass'; const gainNode = ctx.createGain(); @@ -75,7 +77,7 @@ export default function VinylPlayer(): React.ReactElement { return () => { context?.close(); }; - }, [context]); + }, [context]); const playSound = () => { if (!context || !audioBuffer || bufferSourceRef.current) { @@ -110,7 +112,7 @@ export default function VinylPlayer(): React.ReactElement { const state = animationStateRef.current; state.coverHeight = v; state.targetCoverY = (v / C.SLIDER_MAX) * C.SLIDER_MAX; - + if (!bufferSourceRef.current) { playSound(); } @@ -125,13 +127,13 @@ export default function VinylPlayer(): React.ReactElement { 1000 ); camera.position.set(8, 10, 12); - + const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); renderer.setSize(container.clientWidth, container.clientHeight); renderer.setPixelRatio(window.devicePixelRatio); renderer.shadowMap.enabled = true; container.appendChild(renderer.domElement); - + const controls = new OrbitControls(camera, renderer.domElement); controls.enableDamping = true; controls.target.set(0, 2, 0); @@ -143,7 +145,7 @@ export default function VinylPlayer(): React.ReactElement { const initializeLighting = (scene: THREE.Scene) => { const ambientLight = new THREE.AmbientLight(0xffffff, 0.7); scene.add(ambientLight); - + const dirLight = new THREE.DirectionalLight(0xffffff, 1.2); dirLight.position.set(10, 15, 5); dirLight.castShadow = true; @@ -324,7 +326,7 @@ export default function VinylPlayer(): React.ReactElement { // SWM logo on box const logoTexture = new THREE.TextureLoader().load(swmLogo); logoTexture.colorSpace = THREE.SRGBColorSpace; - + const logoMaterialFront = new THREE.MeshBasicMaterial({ map: logoTexture, transparent: true, @@ -335,15 +337,15 @@ export default function VinylPlayer(): React.ReactElement { transparent: true, side: THREE.FrontSide, }); - + const logoWidth = coverWidth * 0.7; const logoHeight = coverBoxHeight * 0.5; const logoPlaneGeometry = new THREE.PlaneGeometry(logoWidth, logoHeight); - + const logoFront = new THREE.Mesh(logoPlaneGeometry, logoMaterialFront); logoFront.position.set(0, coverBoxHeight / 2, coverDepth / 2 + 0.01); coverGroup.add(logoFront); - + const logoBack = new THREE.Mesh(logoPlaneGeometry, logoMaterialBack); logoBack.position.set(0, coverBoxHeight / 2, -coverDepth / 2 - 0.01); logoBack.rotation.y = Math.PI; @@ -450,7 +452,7 @@ export default function VinylPlayer(): React.ReactElement { } const MAX_STRING_LENGTH = C.ANCHOR_Y - C.COVER_TOP_Y; - + const isOnGround = newCoverY < 0.01; if (isOnGround && !state.wasOnGround) { if (bufferSourceRef.current) { @@ -477,13 +479,13 @@ export default function VinylPlayer(): React.ReactElement { const { scene, camera, renderer, controls } = initializeScene(currentMount); initializeLighting(scene); - + const materials = createMaterials(); const recordGroup = createVinylPlayer(scene, materials); const { coverGroup, string, lineMaterial } = createCoverBox(scene, renderer, materials); sceneRefsRef.current = { recordGroup, coverGroup, string }; - + startAnimationLoop(renderer, scene, camera, controls); const handleResize = () => { @@ -546,4 +548,4 @@ export default function VinylPlayer(): React.ReactElement { ); -} \ No newline at end of file +} diff --git a/packages/react-native-audio-api/src/core/BaseAudioContext.ts b/packages/react-native-audio-api/src/core/BaseAudioContext.ts index 3fe2d1eb2..0f73c99ee 100644 --- a/packages/react-native-audio-api/src/core/BaseAudioContext.ts +++ b/packages/react-native-audio-api/src/core/BaseAudioContext.ts @@ -1,24 +1,24 @@ +import { InvalidAccessError, NotSupportedError } from '../errors'; import { IBaseAudioContext } from '../interfaces'; import { + AudioBufferBaseSourceNodeOptions, ContextState, PeriodicWaveConstraints, - AudioBufferBaseSourceNodeOptions, } from '../types'; +import { isWorkletsAvailable, workletsModule } from '../utils'; +import AnalyserNode from './AnalyserNode'; +import AudioBuffer from './AudioBuffer'; +import AudioBufferQueueSourceNode from './AudioBufferQueueSourceNode'; +import AudioBufferSourceNode from './AudioBufferSourceNode'; import AudioDestinationNode from './AudioDestinationNode'; -import OscillatorNode from './OscillatorNode'; -import GainNode from './GainNode'; -import StereoPannerNode from './StereoPannerNode'; import BiquadFilterNode from './BiquadFilterNode'; -import AudioBufferSourceNode from './AudioBufferSourceNode'; -import AudioBuffer from './AudioBuffer'; +import GainNode from './GainNode'; +import OscillatorNode from './OscillatorNode'; import PeriodicWave from './PeriodicWave'; -import AnalyserNode from './AnalyserNode'; -import AudioBufferQueueSourceNode from './AudioBufferQueueSourceNode'; -import StreamerNode from './StreamerNode'; -import { InvalidAccessError, NotSupportedError } from '../errors'; import RecorderAdapterNode from './RecorderAdapterNode'; +import StereoPannerNode from './StereoPannerNode'; +import StreamerNode from './StreamerNode'; import WorkletNode from './WorkletNode'; -import { isWorkletsAvailable, workletsModule } from '../utils'; export default class BaseAudioContext { readonly destination: AudioDestinationNode; diff --git a/packages/react-native-audio-api/src/interfaces.ts b/packages/react-native-audio-api/src/interfaces.ts index a3f355a53..ff79c6237 100644 --- a/packages/react-native-audio-api/src/interfaces.ts +++ b/packages/react-native-audio-api/src/interfaces.ts @@ -1,12 +1,17 @@ +import { AudioEventCallback, AudioEventName } from './events/types'; import { - WindowType, - ContextState, - OscillatorType, BiquadFilterType, ChannelCountMode, ChannelInterpretation, + ContextState, + OscillatorType, + WindowType, } from './types'; -import { AudioEventName, AudioEventCallback } from './events/types'; + +export type ShareableWorkletCallback = ( + audioBuffers: Array, + channelCount: number +) => void; export interface IBaseAudioContext { readonly destination: IAudioDestinationNode; @@ -15,11 +20,11 @@ export interface IBaseAudioContext { readonly currentTime: number; createRecorderAdapter(): IRecorderAdapterNode; - createWorkletNode: ( - shareableWorklet: any, + createWorkletNode( + shareableWorklet: ShareableWorkletCallback, bufferLength: number, inputChannelCount: number - ) => IWorkletNode; + ): IWorkletNode; createOscillator(): IOscillatorNode; createGain(): IGainNode; createStereoPanner(): IStereoPannerNode; diff --git a/packages/react-native-audio-api/src/utils/index.ts b/packages/react-native-audio-api/src/utils/index.ts index 340c125f1..68e5035b3 100644 --- a/packages/react-native-audio-api/src/utils/index.ts +++ b/packages/react-native-audio-api/src/utils/index.ts @@ -1,9 +1,17 @@ +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: any = null; +export let workletsModule: SimplifiedWorkletModule; try { workletsModule = require('react-native-worklets'); From 53eabc74fb5664abb986ed144af720b9951861ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20S=C4=99k?= Date: Mon, 29 Sep 2025 13:26:04 +0200 Subject: [PATCH 02/18] fix: doc edit --- packages/audiodocs/docs/worklets/worklet-node.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/audiodocs/docs/worklets/worklet-node.mdx b/packages/audiodocs/docs/worklets/worklet-node.mdx index d54124209..023edccf3 100644 --- a/packages/audiodocs/docs/worklets/worklet-node.mdx +++ b/packages/audiodocs/docs/worklets/worklet-node.mdx @@ -27,7 +27,7 @@ function App() { }); const audioContext = new AudioContext({ sampleRate: 16000 }); - const worklet = (audioData: Array, inputChannelCount: number) => { + const worklet = (audioData: Array, inputChannelCount: number) => { 'worklet'; // here you have access to the number of input channels and the audio data // audio data is a two dimensional array where first index is the channel number and second is buffer of exactly bufferLength size From d215cc7f45a5aa1625a6044acb86bb9c224f0b85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20S=C4=99k?= Date: Tue, 30 Sep 2025 16:15:44 +0200 Subject: [PATCH 03/18] =?UTF-8?q?chore:=20cooking=20=F0=9F=A7=91=E2=80=8D?= =?UTF-8?q?=F0=9F=8D=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{src => prev_src}/api.ts | 27 ++--- .../{src => prev_src}/api.web.ts | 0 .../core/AudioBufferBaseSourceNode.ts | 0 .../core/AudioBufferQueueSourceNode.ts | 0 .../core/AudioBufferSourceNode.ts | 0 .../{src => prev_src}/core/AudioContext.ts | 0 .../{src => prev_src}/core/AudioRecorder.ts | 0 .../core/BaseAudioContext.ts | 0 .../core/OfflineAudioContext.ts | 0 .../{src => prev_src}/core/OscillatorNode.ts | 0 .../{src => prev_src}/core/PeriodicWave.ts | 0 .../core/RecorderAdapterNode.ts | 0 .../{src => prev_src}/core/StreamerNode.ts | 0 .../{src => prev_src}/core/WorkletNode.ts | 0 .../{src => prev_src}/hooks/useSytemVolume.ts | 0 .../{src => prev_src}/interfaces.ts | 113 +----------------- .../{src => prev_src}/plugin/withAudioAPI.ts | 0 .../specs/NativeAudioAPIModule.ts | 0 .../{src => prev_src}/specs/index.ts | 0 .../{src => prev_src}/system/AudioManager.ts | 0 .../{src => prev_src}/system/index.ts | 0 .../{src => prev_src}/system/types.ts | 0 .../{src => prev_src}/types.ts | 16 --- .../prev_src/utils/index.ts | 21 ++++ .../web-core/AnalyserNode.tsx | 0 .../web-core/AudioBuffer.tsx | 0 .../web-core/AudioBufferSourceNode.tsx | 0 .../web-core/AudioContext.tsx | 0 .../web-core/AudioDestinationNode.tsx | 0 .../{src => prev_src}/web-core/AudioNode.tsx | 0 .../{src => prev_src}/web-core/AudioParam.tsx | 0 .../web-core/AudioScheduledSourceNode.tsx | 0 .../web-core/BaseAudioContext.tsx | 0 .../web-core/BiquadFilterNode.tsx | 0 .../{src => prev_src}/web-core/GainNode.tsx | 0 .../web-core/OfflineAudioContext.tsx | 0 .../web-core/OscillatorNode.tsx | 0 .../web-core/PeriodicWave.tsx | 0 .../web-core/StereoPannerNode.tsx | 0 .../web-core/custom/LoadCustomWasm.ts | 0 .../web-core/custom/index.ts | 0 .../custom/signalsmithStretch/LICENSE.txt | 0 .../custom/signalsmithStretch/README.md | 0 .../signalsmithStretch/SignalsmithStretch.mjs | 0 .../src/core/AnalyserNode.ts | 98 ++------------- .../src/core/AnalyserNode.web.ts | 101 ++++++++++++++++ .../src/core/AudioBuffer.ts | 11 +- .../src/core/AudioDestinationNode.ts | 14 ++- .../src/core/AudioNode.ts | 50 +++++--- .../src/core/AudioParam.ts | 72 ++++++----- .../src/core/AudioScheduledSourceNode.ts | 68 ++++------- .../src/core/AudioScheduledSourceNode.web.ts | 71 +++++++++++ .../src/core/BiquadFilterNode.ts | 45 ++++--- .../src/core/GainNode.ts | 17 ++- .../src/core/StereoPannerNode.ts | 14 ++- .../react-native-audio-api/src/core/index.ts | 7 ++ .../src/events/AudioEventEmitter.ts | 19 ++- .../src/events/AudioEventSubscription.ts | 4 +- .../src/events/index.ts | 6 +- .../react-native-audio-api/src/hooks/.keep | 0 packages/react-native-audio-api/src/index.ts | 1 - .../react-native-audio-api/src/types/index.ts | 1 + .../src/types/internal/IAnalyserNode.ts | 19 +++ .../src/types/internal/IAudioBuffer.ts | 18 +++ .../types/internal/IAudioDestinationNode.ts | 9 ++ .../src/types/internal/IAudioNode.ts | 21 ++++ .../src/types/internal/IAudioParam.ts | 23 ++++ .../internal/IAudioScheduledSourceNode.ts | 11 ++ .../src/types/internal/IBaseAudioContext.ts | 1 + .../src/types/internal/IBiquadFilterNode.ts | 19 +++ .../src/types/internal/IGainNode.ts | 8 ++ .../src/types/internal/IStereoPannerNode.ts | 8 ++ .../src/types/internal/index.ts | 10 ++ .../src/types/properties.ts | 14 +++ .../react-native-audio-api/src/utils/index.ts | 31 ++--- 75 files changed, 587 insertions(+), 381 deletions(-) rename packages/react-native-audio-api/{src => prev_src}/api.ts (97%) rename packages/react-native-audio-api/{src => prev_src}/api.web.ts (100%) rename packages/react-native-audio-api/{src => prev_src}/core/AudioBufferBaseSourceNode.ts (100%) rename packages/react-native-audio-api/{src => prev_src}/core/AudioBufferQueueSourceNode.ts (100%) rename packages/react-native-audio-api/{src => prev_src}/core/AudioBufferSourceNode.ts (100%) rename packages/react-native-audio-api/{src => prev_src}/core/AudioContext.ts (100%) rename packages/react-native-audio-api/{src => prev_src}/core/AudioRecorder.ts (100%) rename packages/react-native-audio-api/{src => prev_src}/core/BaseAudioContext.ts (100%) rename packages/react-native-audio-api/{src => prev_src}/core/OfflineAudioContext.ts (100%) rename packages/react-native-audio-api/{src => prev_src}/core/OscillatorNode.ts (100%) rename packages/react-native-audio-api/{src => prev_src}/core/PeriodicWave.ts (100%) rename packages/react-native-audio-api/{src => prev_src}/core/RecorderAdapterNode.ts (100%) rename packages/react-native-audio-api/{src => prev_src}/core/StreamerNode.ts (100%) rename packages/react-native-audio-api/{src => prev_src}/core/WorkletNode.ts (100%) rename packages/react-native-audio-api/{src => prev_src}/hooks/useSytemVolume.ts (100%) rename packages/react-native-audio-api/{src => prev_src}/interfaces.ts (54%) rename packages/react-native-audio-api/{src => prev_src}/plugin/withAudioAPI.ts (100%) rename packages/react-native-audio-api/{src => prev_src}/specs/NativeAudioAPIModule.ts (100%) rename packages/react-native-audio-api/{src => prev_src}/specs/index.ts (100%) rename packages/react-native-audio-api/{src => prev_src}/system/AudioManager.ts (100%) rename packages/react-native-audio-api/{src => prev_src}/system/index.ts (100%) rename packages/react-native-audio-api/{src => prev_src}/system/types.ts (100%) rename packages/react-native-audio-api/{src => prev_src}/types.ts (68%) create mode 100644 packages/react-native-audio-api/prev_src/utils/index.ts rename packages/react-native-audio-api/{src => prev_src}/web-core/AnalyserNode.tsx (100%) rename packages/react-native-audio-api/{src => prev_src}/web-core/AudioBuffer.tsx (100%) rename packages/react-native-audio-api/{src => prev_src}/web-core/AudioBufferSourceNode.tsx (100%) rename packages/react-native-audio-api/{src => prev_src}/web-core/AudioContext.tsx (100%) rename packages/react-native-audio-api/{src => prev_src}/web-core/AudioDestinationNode.tsx (100%) rename packages/react-native-audio-api/{src => prev_src}/web-core/AudioNode.tsx (100%) rename packages/react-native-audio-api/{src => prev_src}/web-core/AudioParam.tsx (100%) rename packages/react-native-audio-api/{src => prev_src}/web-core/AudioScheduledSourceNode.tsx (100%) rename packages/react-native-audio-api/{src => prev_src}/web-core/BaseAudioContext.tsx (100%) rename packages/react-native-audio-api/{src => prev_src}/web-core/BiquadFilterNode.tsx (100%) rename packages/react-native-audio-api/{src => prev_src}/web-core/GainNode.tsx (100%) rename packages/react-native-audio-api/{src => prev_src}/web-core/OfflineAudioContext.tsx (100%) rename packages/react-native-audio-api/{src => prev_src}/web-core/OscillatorNode.tsx (100%) rename packages/react-native-audio-api/{src => prev_src}/web-core/PeriodicWave.tsx (100%) rename packages/react-native-audio-api/{src => prev_src}/web-core/StereoPannerNode.tsx (100%) rename packages/react-native-audio-api/{src => prev_src}/web-core/custom/LoadCustomWasm.ts (100%) rename packages/react-native-audio-api/{src => prev_src}/web-core/custom/index.ts (100%) rename packages/react-native-audio-api/{src => prev_src}/web-core/custom/signalsmithStretch/LICENSE.txt (100%) rename packages/react-native-audio-api/{src => prev_src}/web-core/custom/signalsmithStretch/README.md (100%) rename packages/react-native-audio-api/{src => prev_src}/web-core/custom/signalsmithStretch/SignalsmithStretch.mjs (100%) create mode 100644 packages/react-native-audio-api/src/core/AnalyserNode.web.ts create mode 100644 packages/react-native-audio-api/src/core/AudioScheduledSourceNode.web.ts create mode 100644 packages/react-native-audio-api/src/core/index.ts create mode 100644 packages/react-native-audio-api/src/hooks/.keep create mode 100644 packages/react-native-audio-api/src/types/index.ts create mode 100644 packages/react-native-audio-api/src/types/internal/IAnalyserNode.ts create mode 100644 packages/react-native-audio-api/src/types/internal/IAudioBuffer.ts create mode 100644 packages/react-native-audio-api/src/types/internal/IAudioDestinationNode.ts create mode 100644 packages/react-native-audio-api/src/types/internal/IAudioNode.ts create mode 100644 packages/react-native-audio-api/src/types/internal/IAudioParam.ts create mode 100644 packages/react-native-audio-api/src/types/internal/IAudioScheduledSourceNode.ts create mode 100644 packages/react-native-audio-api/src/types/internal/IBaseAudioContext.ts create mode 100644 packages/react-native-audio-api/src/types/internal/IBiquadFilterNode.ts create mode 100644 packages/react-native-audio-api/src/types/internal/IGainNode.ts create mode 100644 packages/react-native-audio-api/src/types/internal/IStereoPannerNode.ts create mode 100644 packages/react-native-audio-api/src/types/internal/index.ts create mode 100644 packages/react-native-audio-api/src/types/properties.ts diff --git a/packages/react-native-audio-api/src/api.ts b/packages/react-native-audio-api/prev_src/api.ts similarity index 97% rename from packages/react-native-audio-api/src/api.ts rename to packages/react-native-audio-api/prev_src/api.ts index e45741997..656b799cd 100644 --- a/packages/react-native-audio-api/src/api.ts +++ b/packages/react-native-audio-api/prev_src/api.ts @@ -1,11 +1,10 @@ -import { NativeAudioAPIModule } from './specs'; -import { AudioRecorderOptions } from './types'; import type { IAudioContext, IAudioRecorder, IOfflineAudioContext, - IAudioEventEmitter, } from './interfaces'; +import { NativeAudioAPIModule } from './specs'; +import { AudioRecorderOptions } from './types'; /* eslint-disable no-var */ declare global { @@ -20,8 +19,6 @@ declare global { ) => IOfflineAudioContext; var createAudioRecorder: (options: AudioRecorderOptions) => IAudioRecorder; - - var AudioEventEmitter: IAudioEventEmitter; } /* eslint-disable no-var */ @@ -40,42 +37,42 @@ if ( NativeAudioAPIModule.install(); } -export { default as WorkletNode } from './core/WorkletNode'; -export { default as RecorderAdapterNode } from './core/RecorderAdapterNode'; +export { default as AnalyserNode } from './core/AnalyserNode'; export { default as AudioBuffer } from './core/AudioBuffer'; -export { default as AudioBufferSourceNode } from './core/AudioBufferSourceNode'; export { default as AudioBufferQueueSourceNode } from './core/AudioBufferQueueSourceNode'; +export { default as AudioBufferSourceNode } from './core/AudioBufferSourceNode'; 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 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 AudioRecorder } from './core/AudioRecorder'; export { default as StreamerNode } from './core/StreamerNode'; -export { default as AudioManager } from './system'; +export { default as WorkletNode } from './core/WorkletNode'; export { default as useSystemVolume } from './hooks/useSytemVolume'; +export { default as AudioManager } from './system'; export { - OscillatorType, BiquadFilterType, ChannelCountMode, ChannelInterpretation, ContextState, - WindowType, + OscillatorType, PeriodicWaveConstraints, + WindowType, } from './types'; export { IndexSizeError, InvalidAccessError, InvalidStateError, - RangeError, NotSupportedError, + RangeError, } from './errors'; diff --git a/packages/react-native-audio-api/src/api.web.ts b/packages/react-native-audio-api/prev_src/api.web.ts similarity index 100% rename from packages/react-native-audio-api/src/api.web.ts rename to packages/react-native-audio-api/prev_src/api.web.ts diff --git a/packages/react-native-audio-api/src/core/AudioBufferBaseSourceNode.ts b/packages/react-native-audio-api/prev_src/core/AudioBufferBaseSourceNode.ts similarity index 100% rename from packages/react-native-audio-api/src/core/AudioBufferBaseSourceNode.ts rename to packages/react-native-audio-api/prev_src/core/AudioBufferBaseSourceNode.ts diff --git a/packages/react-native-audio-api/src/core/AudioBufferQueueSourceNode.ts b/packages/react-native-audio-api/prev_src/core/AudioBufferQueueSourceNode.ts similarity index 100% rename from packages/react-native-audio-api/src/core/AudioBufferQueueSourceNode.ts rename to packages/react-native-audio-api/prev_src/core/AudioBufferQueueSourceNode.ts diff --git a/packages/react-native-audio-api/src/core/AudioBufferSourceNode.ts b/packages/react-native-audio-api/prev_src/core/AudioBufferSourceNode.ts similarity index 100% rename from packages/react-native-audio-api/src/core/AudioBufferSourceNode.ts rename to packages/react-native-audio-api/prev_src/core/AudioBufferSourceNode.ts diff --git a/packages/react-native-audio-api/src/core/AudioContext.ts b/packages/react-native-audio-api/prev_src/core/AudioContext.ts similarity index 100% rename from packages/react-native-audio-api/src/core/AudioContext.ts rename to packages/react-native-audio-api/prev_src/core/AudioContext.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 diff --git a/packages/react-native-audio-api/src/core/BaseAudioContext.ts b/packages/react-native-audio-api/prev_src/core/BaseAudioContext.ts similarity index 100% rename from packages/react-native-audio-api/src/core/BaseAudioContext.ts rename to packages/react-native-audio-api/prev_src/core/BaseAudioContext.ts 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/OscillatorNode.ts b/packages/react-native-audio-api/prev_src/core/OscillatorNode.ts similarity index 100% rename from packages/react-native-audio-api/src/core/OscillatorNode.ts rename to packages/react-native-audio-api/prev_src/core/OscillatorNode.ts diff --git a/packages/react-native-audio-api/src/core/PeriodicWave.ts b/packages/react-native-audio-api/prev_src/core/PeriodicWave.ts similarity index 100% rename from packages/react-native-audio-api/src/core/PeriodicWave.ts rename to packages/react-native-audio-api/prev_src/core/PeriodicWave.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/src/hooks/useSytemVolume.ts b/packages/react-native-audio-api/prev_src/hooks/useSytemVolume.ts similarity index 100% rename from packages/react-native-audio-api/src/hooks/useSytemVolume.ts rename to packages/react-native-audio-api/prev_src/hooks/useSytemVolume.ts diff --git a/packages/react-native-audio-api/src/interfaces.ts b/packages/react-native-audio-api/prev_src/interfaces.ts similarity index 54% rename from packages/react-native-audio-api/src/interfaces.ts rename to packages/react-native-audio-api/prev_src/interfaces.ts index ff79c6237..e0a103f49 100644 --- a/packages/react-native-audio-api/src/interfaces.ts +++ b/packages/react-native-audio-api/prev_src/interfaces.ts @@ -1,12 +1,4 @@ -import { AudioEventCallback, AudioEventName } from './events/types'; -import { - BiquadFilterType, - ChannelCountMode, - ChannelInterpretation, - ContextState, - OscillatorType, - WindowType, -} from './types'; +import { ContextState, OscillatorType } from './types'; export type ShareableWorkletCallback = ( audioBuffers: Array, @@ -65,42 +57,6 @@ export interface IOfflineAudioContext extends IBaseAudioContext { 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; @@ -152,64 +108,8 @@ export interface IAudioBufferQueueSourceNode 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 {} @@ -223,14 +123,3 @@ export interface IAudioRecorder { // passing subscriptionId(uint_64 in cpp, string in js) to the cpp onAudioReady: string; } - -export interface IAudioEventEmitter { - addAudioEventListener( - name: Name, - callback: AudioEventCallback - ): string; - removeAudioEventListener( - name: Name, - subscriptionId: string - ): void; -} diff --git a/packages/react-native-audio-api/src/plugin/withAudioAPI.ts b/packages/react-native-audio-api/prev_src/plugin/withAudioAPI.ts similarity index 100% rename from packages/react-native-audio-api/src/plugin/withAudioAPI.ts rename to packages/react-native-audio-api/prev_src/plugin/withAudioAPI.ts diff --git a/packages/react-native-audio-api/src/specs/NativeAudioAPIModule.ts b/packages/react-native-audio-api/prev_src/specs/NativeAudioAPIModule.ts similarity index 100% rename from packages/react-native-audio-api/src/specs/NativeAudioAPIModule.ts rename to packages/react-native-audio-api/prev_src/specs/NativeAudioAPIModule.ts diff --git a/packages/react-native-audio-api/src/specs/index.ts b/packages/react-native-audio-api/prev_src/specs/index.ts similarity index 100% rename from packages/react-native-audio-api/src/specs/index.ts rename to packages/react-native-audio-api/prev_src/specs/index.ts diff --git a/packages/react-native-audio-api/src/system/AudioManager.ts b/packages/react-native-audio-api/prev_src/system/AudioManager.ts similarity index 100% rename from packages/react-native-audio-api/src/system/AudioManager.ts rename to packages/react-native-audio-api/prev_src/system/AudioManager.ts diff --git a/packages/react-native-audio-api/src/system/index.ts b/packages/react-native-audio-api/prev_src/system/index.ts similarity index 100% rename from packages/react-native-audio-api/src/system/index.ts rename to packages/react-native-audio-api/prev_src/system/index.ts diff --git a/packages/react-native-audio-api/src/system/types.ts b/packages/react-native-audio-api/prev_src/system/types.ts similarity index 100% rename from packages/react-native-audio-api/src/system/types.ts rename to packages/react-native-audio-api/prev_src/system/types.ts diff --git a/packages/react-native-audio-api/src/types.ts b/packages/react-native-audio-api/prev_src/types.ts similarity index 68% rename from packages/react-native-audio-api/src/types.ts rename to packages/react-native-audio-api/prev_src/types.ts index 4039affee..4297390dc 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 OscillatorType = @@ -41,8 +27,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/AnalyserNode.tsx b/packages/react-native-audio-api/prev_src/web-core/AnalyserNode.tsx similarity index 100% rename from packages/react-native-audio-api/src/web-core/AnalyserNode.tsx rename to packages/react-native-audio-api/prev_src/web-core/AnalyserNode.tsx diff --git a/packages/react-native-audio-api/src/web-core/AudioBuffer.tsx b/packages/react-native-audio-api/prev_src/web-core/AudioBuffer.tsx similarity index 100% rename from packages/react-native-audio-api/src/web-core/AudioBuffer.tsx rename to packages/react-native-audio-api/prev_src/web-core/AudioBuffer.tsx diff --git a/packages/react-native-audio-api/src/web-core/AudioBufferSourceNode.tsx b/packages/react-native-audio-api/prev_src/web-core/AudioBufferSourceNode.tsx similarity index 100% rename from packages/react-native-audio-api/src/web-core/AudioBufferSourceNode.tsx rename to packages/react-native-audio-api/prev_src/web-core/AudioBufferSourceNode.tsx 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/AudioDestinationNode.tsx b/packages/react-native-audio-api/prev_src/web-core/AudioDestinationNode.tsx similarity index 100% rename from packages/react-native-audio-api/src/web-core/AudioDestinationNode.tsx rename to packages/react-native-audio-api/prev_src/web-core/AudioDestinationNode.tsx diff --git a/packages/react-native-audio-api/src/web-core/AudioNode.tsx b/packages/react-native-audio-api/prev_src/web-core/AudioNode.tsx similarity index 100% rename from packages/react-native-audio-api/src/web-core/AudioNode.tsx rename to packages/react-native-audio-api/prev_src/web-core/AudioNode.tsx diff --git a/packages/react-native-audio-api/src/web-core/AudioParam.tsx b/packages/react-native-audio-api/prev_src/web-core/AudioParam.tsx similarity index 100% rename from packages/react-native-audio-api/src/web-core/AudioParam.tsx rename to packages/react-native-audio-api/prev_src/web-core/AudioParam.tsx diff --git a/packages/react-native-audio-api/src/web-core/AudioScheduledSourceNode.tsx b/packages/react-native-audio-api/prev_src/web-core/AudioScheduledSourceNode.tsx similarity index 100% rename from packages/react-native-audio-api/src/web-core/AudioScheduledSourceNode.tsx rename to packages/react-native-audio-api/prev_src/web-core/AudioScheduledSourceNode.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/BiquadFilterNode.tsx b/packages/react-native-audio-api/prev_src/web-core/BiquadFilterNode.tsx similarity index 100% rename from packages/react-native-audio-api/src/web-core/BiquadFilterNode.tsx rename to packages/react-native-audio-api/prev_src/web-core/BiquadFilterNode.tsx diff --git a/packages/react-native-audio-api/src/web-core/GainNode.tsx b/packages/react-native-audio-api/prev_src/web-core/GainNode.tsx similarity index 100% rename from packages/react-native-audio-api/src/web-core/GainNode.tsx rename to packages/react-native-audio-api/prev_src/web-core/GainNode.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/web-core/OscillatorNode.tsx b/packages/react-native-audio-api/prev_src/web-core/OscillatorNode.tsx similarity index 100% rename from packages/react-native-audio-api/src/web-core/OscillatorNode.tsx rename to packages/react-native-audio-api/prev_src/web-core/OscillatorNode.tsx diff --git a/packages/react-native-audio-api/src/web-core/PeriodicWave.tsx b/packages/react-native-audio-api/prev_src/web-core/PeriodicWave.tsx similarity index 100% rename from packages/react-native-audio-api/src/web-core/PeriodicWave.tsx rename to packages/react-native-audio-api/prev_src/web-core/PeriodicWave.tsx diff --git a/packages/react-native-audio-api/src/web-core/StereoPannerNode.tsx b/packages/react-native-audio-api/prev_src/web-core/StereoPannerNode.tsx similarity index 100% rename from packages/react-native-audio-api/src/web-core/StereoPannerNode.tsx rename to packages/react-native-audio-api/prev_src/web-core/StereoPannerNode.tsx diff --git a/packages/react-native-audio-api/src/web-core/custom/LoadCustomWasm.ts b/packages/react-native-audio-api/prev_src/web-core/custom/LoadCustomWasm.ts similarity index 100% rename from packages/react-native-audio-api/src/web-core/custom/LoadCustomWasm.ts rename to packages/react-native-audio-api/prev_src/web-core/custom/LoadCustomWasm.ts diff --git a/packages/react-native-audio-api/src/web-core/custom/index.ts b/packages/react-native-audio-api/prev_src/web-core/custom/index.ts similarity index 100% rename from packages/react-native-audio-api/src/web-core/custom/index.ts rename to packages/react-native-audio-api/prev_src/web-core/custom/index.ts diff --git a/packages/react-native-audio-api/src/web-core/custom/signalsmithStretch/LICENSE.txt b/packages/react-native-audio-api/prev_src/web-core/custom/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/prev_src/web-core/custom/signalsmithStretch/LICENSE.txt diff --git a/packages/react-native-audio-api/src/web-core/custom/signalsmithStretch/README.md b/packages/react-native-audio-api/prev_src/web-core/custom/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/prev_src/web-core/custom/signalsmithStretch/README.md diff --git a/packages/react-native-audio-api/src/web-core/custom/signalsmithStretch/SignalsmithStretch.mjs b/packages/react-native-audio-api/prev_src/web-core/custom/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/prev_src/web-core/custom/signalsmithStretch/SignalsmithStretch.mjs diff --git a/packages/react-native-audio-api/src/core/AnalyserNode.ts b/packages/react-native-audio-api/src/core/AnalyserNode.ts index d2d8176e1..ab100f274 100644 --- a/packages/react-native-audio-api/src/core/AnalyserNode.ts +++ b/packages/react-native-audio-api/src/core/AnalyserNode.ts @@ -1,94 +1,16 @@ -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; - } - +import type { WindowType } from '../types'; +import type { IAnalyserNode, IBaseAudioContext } from '../types/internal'; +import AnalyserNode from './AnalyserNode.web'; + +export default class AnalyserNodeNative< + TContext extends IBaseAudioContext, + NContext extends IBaseAudioContext, +> extends AnalyserNode { public get window(): WindowType { - return (this.node as IAnalyserNode).window; + 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); + (this.node as IAnalyserNode).window = value; } } diff --git a/packages/react-native-audio-api/src/core/AnalyserNode.web.ts b/packages/react-native-audio-api/src/core/AnalyserNode.web.ts new file mode 100644 index 000000000..fc257c1c4 --- /dev/null +++ b/packages/react-native-audio-api/src/core/AnalyserNode.web.ts @@ -0,0 +1,101 @@ +import { IndexSizeError } from '../errors'; +import type { WindowType } from '../types'; +import type { IAnalyserNode, IBaseAudioContext } from '../types/internal'; +import { availabilityWarn } from '../utils'; +import AudioNode from './AudioNode'; + +const allowedFFTSize = [ + 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, +]; + +export default class AnalyserNode< + TContext extends IBaseAudioContext, + NContext extends IBaseAudioContext, + > + extends AudioNode + implements IAnalyserNode +{ + public get fftSize(): number { + return (this.node as IAnalyserNode).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 as IAnalyserNode).fftSize = value; + } + + public get minDecibels(): number { + return (this.node as IAnalyserNode).minDecibels; + } + + public set minDecibels(value: number) { + if (value >= (this.node as IAnalyserNode).maxDecibels) { + throw new IndexSizeError( + `The minDecibels value (${value}) must be less than maxDecibels` + ); + } + + (this.node as IAnalyserNode).minDecibels = 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 maxDecibels(): number { + return (this.node as IAnalyserNode).maxDecibels; + } + + public set maxDecibels(value: number) { + if (value <= (this.node as IAnalyserNode).minDecibels) { + throw new IndexSizeError( + `The maxDecibels value (${value}) must be greater than minDecibels` + ); + } + + (this.node as IAnalyserNode).maxDecibels = value; + } + + public get window(): WindowType { + return 'blackman'; + } + + public set window(_value: WindowType) { + availabilityWarn('window', 'web', '/'); + } + + 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..e5564bd5e 100644 --- a/packages/react-native-audio-api/src/core/AudioBuffer.ts +++ b/packages/react-native-audio-api/src/core/AudioBuffer.ts @@ -1,7 +1,7 @@ -import { IAudioBuffer } from '../interfaces'; import { IndexSizeError } from '../errors'; +import type { IAudioBuffer } from '../types/internal'; -export default class AudioBuffer { +export default class AudioBuffer implements IAudioBuffer { readonly length: number; readonly duration: number; readonly sampleRate: number; @@ -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/AudioDestinationNode.ts b/packages/react-native-audio-api/src/core/AudioDestinationNode.ts index f1d850e90..95abc0591 100644 --- a/packages/react-native-audio-api/src/core/AudioDestinationNode.ts +++ b/packages/react-native-audio-api/src/core/AudioDestinationNode.ts @@ -1,3 +1,15 @@ +import type { + IAudioDestinationNode, + IBaseAudioContext, +} from '../types/internal'; import AudioNode from './AudioNode'; -export default class AudioDestinationNode extends AudioNode {} +export default class AudioDestinationNode< + TContext extends IBaseAudioContext, + NContext extends IBaseAudioContext, + > + extends AudioNode + implements IAudioDestinationNode { + // TODO: implement on native side + // readonly maxChannelCount: number; +} diff --git a/packages/react-native-audio-api/src/core/AudioNode.ts b/packages/react-native-audio-api/src/core/AudioNode.ts index 2e19d51d1..09d848b3c 100644 --- a/packages/react-native-audio-api/src/core/AudioNode.ts +++ b/packages/react-native-audio-api/src/core/AudioNode.ts @@ -1,19 +1,22 @@ -import { IAudioNode } from '../interfaces'; -import AudioParam from './AudioParam'; import { ChannelCountMode, ChannelInterpretation } from '../types'; -import BaseAudioContext from './BaseAudioContext'; -import { InvalidAccessError } from '../errors'; +import type { IAudioNode, IBaseAudioContext } from '../types/internal'; +import AudioParam from './AudioParam'; -export default class AudioNode { - readonly context: BaseAudioContext; +export default class AudioNode< + TContext extends IBaseAudioContext, + NContext extends IBaseAudioContext, +> implements IAudioNode +{ + 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: IAudioNode; + + constructor(context: TContext, node: IAudioNode) { this.context = context; this.node = node; this.numberOfInputs = this.node.numberOfInputs; @@ -23,27 +26,40 @@ export default class AudioNode { this.channelInterpretation = this.node.channelInterpretation; } - public connect(destination: AudioNode | AudioParam): AudioNode | AudioParam { + public connect( + destination: AudioNode + ): AudioNode; + + public connect(destination: AudioParam): void; + public connect( + destination: AudioNode | AudioParam + ): AudioNode | 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: AudioNode): 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..5b6b8a1ef 100644 --- a/packages/react-native-audio-api/src/core/AudioParam.ts +++ b/packages/react-native-audio-api/src/core/AudioParam.ts @@ -1,51 +1,59 @@ -import { IAudioParam } from '../interfaces'; -import { RangeError, InvalidStateError } from '../errors'; -import BaseAudioContext from './BaseAudioContext'; - -export default class AudioParam { +import { InvalidStateError } from '../errors'; +import { IAudioParam, IBaseAudioContext } from '../types/internal'; + +export default class AudioParam< + TContext extends IBaseAudioContext, + NContext extends IBaseAudioContext, +> implements IAudioParam +{ 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: IAudioParam; // IAudioParam + readonly context: TContext; + + constructor(param: IAudioParam, 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 +61,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 +77,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 +90,7 @@ export default class AudioParam { ); } - this.audioParam.setTargetAtTime(target, startTime, timeConstant); + this.param.setTargetAtTime(target, startTime, timeConstant); return this; } @@ -91,7 +99,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 +116,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 index 8f3366187..2b049f651 100644 --- a/packages/react-native-audio-api/src/core/AudioScheduledSourceNode.ts +++ b/packages/react-native-audio-api/src/core/AudioScheduledSourceNode.ts @@ -1,69 +1,47 @@ -import { IAudioScheduledSourceNode } from '../interfaces'; -import AudioNode from './AudioNode'; -import { InvalidStateError, RangeError } from '../errors'; -import { OnEndedEventType } from '../events/types'; import { AudioEventEmitter, AudioEventSubscription } from '../events'; +import type { + IAudioScheduledSourceNode, + IBaseAudioContext, +} from '../types/internal'; +import type { OnEndedEventCallback } from './AudioScheduledSourceNode.web'; +import AudioScheduledSourceNode from './AudioScheduledSourceNode.web'; + +interface INativeAudioScheduledSourceNode + extends IAudioScheduledSourceNode { + onEnded: string; // subscriptionId or '0' for none +} -export default class AudioScheduledSourceNode extends AudioNode { - protected hasBeenStarted: boolean = false; +export default class AudioScheduledSourceNodeNative< + TContext extends IBaseAudioContext, + NContext extends IBaseAudioContext, +> extends AudioScheduledSourceNode { 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); - } + private onEndedCallbackNative: OnEndedEventCallback | null = null; - public get onEnded(): ((event: OnEndedEventType) => void) | undefined { - return this.onEndedCallback; + public get onEnded(): OnEndedEventCallback | undefined { + return this.onEndedCallbackNative ?? undefined; } - public set onEnded(callback: ((event: OnEndedEventType) => void) | null) { + public set onEnded(callback: OnEndedEventCallback | null) { if (!callback) { - (this.node as IAudioScheduledSourceNode).onEnded = '0'; + (this.node as INativeAudioScheduledSourceNode).onEnded = '0'; this.onEndedSubscription?.remove(); this.onEndedSubscription = undefined; - this.onEndedCallback = undefined; + this.onEndedCallbackNative = null; return; } - this.onEndedCallback = callback; + this.onEndedCallbackNative = callback; this.onEndedSubscription = this.audioEventEmitter.addAudioEventListener( 'ended', callback ); - (this.node as IAudioScheduledSourceNode).onEnded = + (this.node as INativeAudioScheduledSourceNode).onEnded = this.onEndedSubscription.subscriptionId; } } diff --git a/packages/react-native-audio-api/src/core/AudioScheduledSourceNode.web.ts b/packages/react-native-audio-api/src/core/AudioScheduledSourceNode.web.ts new file mode 100644 index 000000000..b447a49ce --- /dev/null +++ b/packages/react-native-audio-api/src/core/AudioScheduledSourceNode.web.ts @@ -0,0 +1,71 @@ +import { InvalidStateError, RangeError } from '../errors'; +import type { OnEndedEventType } from '../events/types'; +import type { + IAudioScheduledSourceNode, + IBaseAudioContext, +} from '../types/internal'; +import AudioNode from './AudioNode'; + +export type OnEndedEventCallback = (event: OnEndedEventType) => void; + +export default class AudioScheduledSourceNode< + TContext extends IBaseAudioContext, + NContext extends IBaseAudioContext, + > + extends AudioNode + implements IAudioScheduledSourceNode +{ + protected hasBeenStarted: boolean = false; + private onEndedCallback: OnEndedEventCallback | null = null; + + 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(): OnEndedEventCallback | undefined { + return this.onEndedCallback ?? undefined; + } + + public set onEnded(callback: OnEndedEventCallback | null) { + if (!callback) { + this.onEndedCallback = null; + (this.node as unknown as globalThis.AudioScheduledSourceNode).onended = + null; + + return; + } + + this.onEndedCallback = callback; + (this.node as unknown as globalThis.AudioScheduledSourceNode).onended = + () => { + this.onEndedCallback?.({ bufferId: undefined }); + }; + } +} diff --git a/packages/react-native-audio-api/src/core/BiquadFilterNode.ts b/packages/react-native-audio-api/src/core/BiquadFilterNode.ts index 68db7c02a..092bd06b3 100644 --- a/packages/react-native-audio-api/src/core/BiquadFilterNode.ts +++ b/packages/react-native-audio-api/src/core/BiquadFilterNode.ts @@ -1,30 +1,42 @@ import { InvalidAccessError } from '../errors'; -import { IBiquadFilterNode } from '../interfaces'; +import { BiquadFilterType } from '../types'; +import type { IBaseAudioContext, IBiquadFilterNode } from '../types/internal'; 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; +export default class BiquadFilterNode< + TContext extends IBaseAudioContext, + NContext extends IBaseAudioContext, + > + extends AudioNode + implements IBiquadFilterNode +{ + readonly frequency: AudioParam; + readonly detune: AudioParam; + readonly Q: AudioParam; + readonly gain: AudioParam; - constructor(context: BaseAudioContext, biquadFilter: IBiquadFilterNode) { + constructor(context: TContext, 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); + + 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 as IBiquadFilterNode).type; + return (this.node as IBiquadFilterNode).type; } public set type(value: BiquadFilterType) { - (this.node as IBiquadFilterNode).type = value; + (this.node as IBiquadFilterNode).type = value; } public getFrequencyResponse( @@ -40,7 +52,8 @@ export default class BiquadFilterNode extends AudioNode { `The lengths of the arrays are not the same frequencyArray: ${frequencyArray.length}, magResponseOutput: ${magResponseOutput.length}, phaseResponseOutput: ${phaseResponseOutput.length}` ); } - (this.node as IBiquadFilterNode).getFrequencyResponse( + + (this.node as IBiquadFilterNode).getFrequencyResponse( frequencyArray, magResponseOutput, phaseResponseOutput diff --git a/packages/react-native-audio-api/src/core/GainNode.ts b/packages/react-native-audio-api/src/core/GainNode.ts index a216f1365..67f1be70f 100644 --- a/packages/react-native-audio-api/src/core/GainNode.ts +++ b/packages/react-native-audio-api/src/core/GainNode.ts @@ -1,13 +1,18 @@ -import { IGainNode } from '../interfaces'; +import type { IBaseAudioContext, IGainNode } from '../types/internal'; import AudioNode from './AudioNode'; import AudioParam from './AudioParam'; -import BaseAudioContext from './BaseAudioContext'; -export default class GainNode extends AudioNode { - readonly gain: AudioParam; +export default class GainNode< + TContext extends IBaseAudioContext, + NContext extends IBaseAudioContext, + > + extends AudioNode + implements IGainNode +{ + readonly gain: AudioParam; - constructor(context: BaseAudioContext, gain: IGainNode) { + constructor(context: TContext, gain: IGainNode) { super(context, gain); - this.gain = new AudioParam(gain.gain, context); + this.gain = new AudioParam(gain.gain, context); } } diff --git a/packages/react-native-audio-api/src/core/StereoPannerNode.ts b/packages/react-native-audio-api/src/core/StereoPannerNode.ts index a8fa77006..e040700de 100644 --- a/packages/react-native-audio-api/src/core/StereoPannerNode.ts +++ b/packages/react-native-audio-api/src/core/StereoPannerNode.ts @@ -1,13 +1,15 @@ -import { IStereoPannerNode } from '../interfaces'; +import type { IBaseAudioContext, IStereoPannerNode } from '../types/internal'; import AudioNode from './AudioNode'; import AudioParam from './AudioParam'; -import BaseAudioContext from './BaseAudioContext'; -export default class StereoPannerNode extends AudioNode { - readonly pan: AudioParam; +export default class StereoPannerNode< + TContext extends IBaseAudioContext, + NContext extends IBaseAudioContext, +> extends AudioNode { + readonly pan: AudioParam; - constructor(context: BaseAudioContext, pan: IStereoPannerNode) { + constructor(context: TContext, pan: IStereoPannerNode) { super(context, pan); - this.pan = new AudioParam(pan.pan, context); + this.pan = new AudioParam(pan.pan, context); } } 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..cc25c56eb --- /dev/null +++ b/packages/react-native-audio-api/src/core/index.ts @@ -0,0 +1,7 @@ +export { default as AudioBuffer } from './AudioBuffer'; +export { default as AudioDestinationNode } from './AudioDestinationNode'; +export { default as AudioNode } from './AudioNode'; +export { default as AudioParam } from './AudioParam'; +export { default as BiquadFilterNode } from './BiquadFilterNode'; +export { default as GainNode } from './GainNode'; +export { default as StereoPannerNode } from './StereoPannerNode'; 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..d8597897b 100644 --- a/packages/react-native-audio-api/src/events/index.ts +++ b/packages/react-native-audio-api/src/events/index.ts @@ -1,4 +1,2 @@ -import AudioEventEmitter from './AudioEventEmitter'; -import AudioEventSubscription from './AudioEventSubscription'; - -export { AudioEventEmitter, AudioEventSubscription }; +export { default as AudioEventEmitter } from './AudioEventEmitter'; +export { default as AudioEventSubscription } from './AudioEventSubscription'; diff --git a/packages/react-native-audio-api/src/hooks/.keep b/packages/react-native-audio-api/src/hooks/.keep new file mode 100644 index 000000000..e69de29bb 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/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/internal/IAnalyserNode.ts b/packages/react-native-audio-api/src/types/internal/IAnalyserNode.ts new file mode 100644 index 000000000..e0c119afe --- /dev/null +++ b/packages/react-native-audio-api/src/types/internal/IAnalyserNode.ts @@ -0,0 +1,19 @@ +import type { WindowType } from '../properties'; +import type IAudioNode from './IAudioNode'; +import type IBaseAudioContext from './IBaseAudioContext'; + +export default interface IAnalyserNode + extends IAudioNode { + 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/internal/IAudioBuffer.ts b/packages/react-native-audio-api/src/types/internal/IAudioBuffer.ts new file mode 100644 index 000000000..ed6ef6d2b --- /dev/null +++ b/packages/react-native-audio-api/src/types/internal/IAudioBuffer.ts @@ -0,0 +1,18 @@ +export default 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; +} diff --git a/packages/react-native-audio-api/src/types/internal/IAudioDestinationNode.ts b/packages/react-native-audio-api/src/types/internal/IAudioDestinationNode.ts new file mode 100644 index 000000000..808eea015 --- /dev/null +++ b/packages/react-native-audio-api/src/types/internal/IAudioDestinationNode.ts @@ -0,0 +1,9 @@ +import type IAudioNode from './IAudioNode'; +import type IBaseAudioContext from './IBaseAudioContext'; + +export default interface IAudioDestinationNode< + TContext extends IBaseAudioContext, +> extends IAudioNode { + // TODO: implement on native side + // readonly maxChannelCount: number; +} diff --git a/packages/react-native-audio-api/src/types/internal/IAudioNode.ts b/packages/react-native-audio-api/src/types/internal/IAudioNode.ts new file mode 100644 index 000000000..9714362d9 --- /dev/null +++ b/packages/react-native-audio-api/src/types/internal/IAudioNode.ts @@ -0,0 +1,21 @@ +import type IAudioParam from './IAudioParam'; +import type IBaseAudioContext from './IBaseAudioContext'; + +export default interface IAudioNode { + readonly context: TContext; + readonly numberOfInputs: number; + readonly numberOfOutputs: number; + readonly channelCount: number; + readonly channelCountMode: ChannelCountMode; + readonly channelInterpretation: ChannelInterpretation; + + connect>( + destination: D + ): D; + connect(destination: IAudioParam): void; + + disconnect>( + destination?: D + ): void; + disconnect(destination?: IAudioParam): void; +} diff --git a/packages/react-native-audio-api/src/types/internal/IAudioParam.ts b/packages/react-native-audio-api/src/types/internal/IAudioParam.ts new file mode 100644 index 000000000..c8879329e --- /dev/null +++ b/packages/react-native-audio-api/src/types/internal/IAudioParam.ts @@ -0,0 +1,23 @@ +export default interface IAudioParam { + readonly defaultValue: number; + readonly minValue: number; + readonly maxValue: number; + + value: number; + + cancelAndHoldAtTime: (cancelTime: number) => IAudioParam; + cancelScheduledValues: (cancelTime: number) => IAudioParam; + exponentialRampToValueAtTime: (value: number, endTime: number) => IAudioParam; + linearRampToValueAtTime: (value: number, endTime: number) => IAudioParam; + setTargetAtTime: ( + target: number, + startTime: number, + timeConstant: number + ) => IAudioParam; + setValueAtTime: (value: number, startTime: number) => IAudioParam; + setValueCurveAtTime: ( + values: Float32Array, + startTime: number, + duration: number + ) => IAudioParam; +} diff --git a/packages/react-native-audio-api/src/types/internal/IAudioScheduledSourceNode.ts b/packages/react-native-audio-api/src/types/internal/IAudioScheduledSourceNode.ts new file mode 100644 index 000000000..fbd25e05f --- /dev/null +++ b/packages/react-native-audio-api/src/types/internal/IAudioScheduledSourceNode.ts @@ -0,0 +1,11 @@ +import type IAudioNode from './IAudioNode'; +import type IBaseAudioContext from './IBaseAudioContext'; + +export default interface IAudioScheduledSourceNode< + TContext extends IBaseAudioContext, +> extends IAudioNode { + start(when?: number): void; + stop(when?: number): void; + + // TODO: what to do about onEnded=string(native) onEnded=function(TS) and onended=function(web) +} diff --git a/packages/react-native-audio-api/src/types/internal/IBaseAudioContext.ts b/packages/react-native-audio-api/src/types/internal/IBaseAudioContext.ts new file mode 100644 index 000000000..9d7e4c699 --- /dev/null +++ b/packages/react-native-audio-api/src/types/internal/IBaseAudioContext.ts @@ -0,0 +1 @@ +export default interface IBaseAudioContext {} diff --git a/packages/react-native-audio-api/src/types/internal/IBiquadFilterNode.ts b/packages/react-native-audio-api/src/types/internal/IBiquadFilterNode.ts new file mode 100644 index 000000000..c5c00dab3 --- /dev/null +++ b/packages/react-native-audio-api/src/types/internal/IBiquadFilterNode.ts @@ -0,0 +1,19 @@ +import type { BiquadFilterType } from '../properties'; +import type IAudioNode from './IAudioNode'; +import type IAudioParam from './IAudioParam'; +import type IBaseAudioContext from './IBaseAudioContext'; + +export default interface IStereoPannerNode + extends IAudioNode { + readonly frequency: IAudioParam; + readonly detune: IAudioParam; + readonly Q: IAudioParam; + readonly gain: IAudioParam; + type: BiquadFilterType; + + getFrequencyResponse( + frequencyArray: Float32Array, + magResponseOutput: Float32Array, + phaseResponseOutput: Float32Array + ): void; +} diff --git a/packages/react-native-audio-api/src/types/internal/IGainNode.ts b/packages/react-native-audio-api/src/types/internal/IGainNode.ts new file mode 100644 index 000000000..72cd2577c --- /dev/null +++ b/packages/react-native-audio-api/src/types/internal/IGainNode.ts @@ -0,0 +1,8 @@ +import type IAudioNode from './IAudioNode'; +import type IAudioParam from './IAudioParam'; +import type IBaseAudioContext from './IBaseAudioContext'; + +export default interface IGainNode + extends IAudioNode { + readonly gain: IAudioParam; +} diff --git a/packages/react-native-audio-api/src/types/internal/IStereoPannerNode.ts b/packages/react-native-audio-api/src/types/internal/IStereoPannerNode.ts new file mode 100644 index 000000000..dcf7d6a12 --- /dev/null +++ b/packages/react-native-audio-api/src/types/internal/IStereoPannerNode.ts @@ -0,0 +1,8 @@ +import type IAudioNode from './IAudioNode'; +import type IAudioParam from './IAudioParam'; +import type IBaseAudioContext from './IBaseAudioContext'; + +export default interface IStereoPannerNode + extends IAudioNode { + readonly pan: IAudioParam; +} diff --git a/packages/react-native-audio-api/src/types/internal/index.ts b/packages/react-native-audio-api/src/types/internal/index.ts new file mode 100644 index 000000000..35ba1f98d --- /dev/null +++ b/packages/react-native-audio-api/src/types/internal/index.ts @@ -0,0 +1,10 @@ +export { default as IAnalyserNode } from './IAnalyserNode'; +export { default as IAudioBuffer } from './IAudioBuffer'; +export { default as IAudioDestinationNode } from './IAudioDestinationNode'; +export { default as IAudioNode } from './IAudioNode'; +export { default as IAudioParam } from './IAudioParam'; +export { default as IAudioScheduledSourceNode } from './IAudioScheduledSourceNode'; +export { default as IBaseAudioContext } from './IBaseAudioContext'; +export { default as IBiquadFilterNode } from './IBiquadFilterNode'; +export { default as IGainNode } from './IGainNode'; +export { default as IStereoPannerNode } from './IStereoPannerNode'; 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..81752176a --- /dev/null +++ b/packages/react-native-audio-api/src/types/properties.ts @@ -0,0 +1,14 @@ +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'; diff --git a/packages/react-native-audio-api/src/utils/index.ts b/packages/react-native-audio-api/src/utils/index.ts index 68e5035b3..c1bf6add8 100644 --- a/packages/react-native-audio-api/src/utils/index.ts +++ b/packages/react-native-audio-api/src/utils/index.ts @@ -1,21 +1,22 @@ -import type { ShareableWorkletCallback } from '../interfaces'; +export const constants = { + baseUrl: 'https://docs.swmansion.com/react-native-audio-api', +} as const; -interface SimplifiedWorkletModule { - makeShareableCloneRecursive: ( - workletCallback: ShareableWorkletCallback - ) => ShareableWorkletCallback; +export function makeDocLink(path: string) { + return `${constants.baseUrl}${path}`; } -export function clamp(value: number, min: number, max: number): number { - return Math.min(Math.max(value, min), max); -} +export function availabilityWarn( + feature: string, + platform: 'web' | 'native' = 'web', + docPath?: string +) { + const baseMsg = `The ${feature} is not available on ${platform} platform.`; -export let isWorkletsAvailable = false; -export let workletsModule: SimplifiedWorkletModule; + if (!docPath) { + console.warn(baseMsg); + return; + } -try { - workletsModule = require('react-native-worklets'); - isWorkletsAvailable = true; -} catch (error) { - isWorkletsAvailable = false; + console.warn(`${baseMsg} See ${makeDocLink(docPath)} for more information.`); } From 433ee32f00f42ddadc5e8a91ea268e1e1240cba5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20S=C4=99k?= Date: Tue, 30 Sep 2025 16:59:30 +0200 Subject: [PATCH 04/18] fix: audio manager interface --- .../src/events/index.ts | 2 + .../specs/NativeAudioAPIModule.ts | 0 .../{prev_src => src}/specs/index.ts | 0 .../system/AudioManager.native.ts} | 26 +++--- .../src/system/AudioManager.ts | 92 +++++++++++++++++++ .../{prev_src => src}/system/index.ts | 0 .../src/system/interface.ts | 34 +++++++ .../{prev_src => src}/system/types.ts | 0 8 files changed, 143 insertions(+), 11 deletions(-) rename packages/react-native-audio-api/{prev_src => src}/specs/NativeAudioAPIModule.ts (100%) rename packages/react-native-audio-api/{prev_src => src}/specs/index.ts (100%) rename packages/react-native-audio-api/{prev_src/system/AudioManager.ts => src/system/AudioManager.native.ts} (93%) create mode 100644 packages/react-native-audio-api/src/system/AudioManager.ts rename packages/react-native-audio-api/{prev_src => src}/system/index.ts (100%) create mode 100644 packages/react-native-audio-api/src/system/interface.ts rename packages/react-native-audio-api/{prev_src => src}/system/types.ts (100%) diff --git a/packages/react-native-audio-api/src/events/index.ts b/packages/react-native-audio-api/src/events/index.ts index d8597897b..6c453cfab 100644 --- a/packages/react-native-audio-api/src/events/index.ts +++ b/packages/react-native-audio-api/src/events/index.ts @@ -1,2 +1,4 @@ export { default as AudioEventEmitter } from './AudioEventEmitter'; export { default as AudioEventSubscription } from './AudioEventSubscription'; + +export * from './types'; diff --git a/packages/react-native-audio-api/prev_src/specs/NativeAudioAPIModule.ts b/packages/react-native-audio-api/src/specs/NativeAudioAPIModule.ts similarity index 100% rename from packages/react-native-audio-api/prev_src/specs/NativeAudioAPIModule.ts rename to packages/react-native-audio-api/src/specs/NativeAudioAPIModule.ts diff --git a/packages/react-native-audio-api/prev_src/specs/index.ts b/packages/react-native-audio-api/src/specs/index.ts similarity index 100% rename from packages/react-native-audio-api/prev_src/specs/index.ts rename to packages/react-native-audio-api/src/specs/index.ts diff --git a/packages/react-native-audio-api/prev_src/system/AudioManager.ts b/packages/react-native-audio-api/src/system/AudioManager.native.ts similarity index 93% rename from packages/react-native-audio-api/prev_src/system/AudioManager.ts rename to packages/react-native-audio-api/src/system/AudioManager.native.ts index 70a116ee4..b8cf4cfa4 100644 --- a/packages/react-native-audio-api/prev_src/system/AudioManager.ts +++ b/packages/react-native-audio-api/src/system/AudioManager.native.ts @@ -1,16 +1,19 @@ -import { - SessionOptions, +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, - 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) { @@ -22,8 +25,9 @@ if (global.AudioEventEmitter == null) { NativeAudioAPIModule.install(); } -class AudioManager { +class AudioManager implements IAudioManager { private readonly audioEventEmitter: AudioEventEmitter; + constructor() { this.audioEventEmitter = new AudioEventEmitter(global.AudioEventEmitter); } diff --git a/packages/react-native-audio-api/src/system/AudioManager.ts b/packages/react-native-audio-api/src/system/AudioManager.ts new file mode 100644 index 000000000..cc778ef99 --- /dev/null +++ b/packages/react-native-audio-api/src/system/AudioManager.ts @@ -0,0 +1,92 @@ +import type { + AudioEventSubscription, + RemoteCommandEventName, + SystemEventCallback, + SystemEventName, +} from '../events'; +import { availabilityWarn } from '../utils'; +import IAudioManager from './interface'; +import type { + AudioDevicesInfo, + LockScreenInfo, + PermissionStatus, + SessionOptions, +} from './types'; + +class NoopSubscription { + remove(): void { + // noop + } +} + +class AudioManager implements IAudioManager { + getDevicePreferredSampleRate(): number { + availabilityWarn('AudioManager.getDevicePreferredSampleRate', 'web'); + return 0; + } + + setAudioSessionActivity(_enabled: boolean): Promise { + availabilityWarn('AudioManager.setAudioSessionActivity', 'web'); + return Promise.resolve(false); + } + + setAudioSessionOptions(_options: SessionOptions) { + availabilityWarn('AudioManager.setAudioSessionOptions', 'web'); + } + + setLockScreenInfo(_info: LockScreenInfo) { + availabilityWarn('AudioManager.setLockScreenInfo', 'web'); + } + + resetLockScreenInfo() { + availabilityWarn('AudioManager.resetLockScreenInfo', 'web'); + } + + observeAudioInterruptions(_enabled: boolean) { + availabilityWarn('AudioManager.observeAudioInterruptions', 'web'); + } + + activelyReclaimSession(_enabled: boolean): void { + availabilityWarn('AudioManager.activelyReclaimSession', 'web'); + } + + observeVolumeChanges(_enabled: boolean): void { + availabilityWarn('AudioManager.observeVolumeChanges', 'web'); + } + + enableRemoteCommand(_name: RemoteCommandEventName, _enabled: boolean): void { + availabilityWarn('AudioManager.enableRemoteCommand', 'web'); + } + + addSystemEventListener( + _name: Name, + _callback: SystemEventCallback + ): AudioEventSubscription { + availabilityWarn('AudioManager.addSystemEventListener', 'web'); + return new NoopSubscription() as unknown as AudioEventSubscription; + } + + async requestRecordingPermissions(): Promise { + // TODO: could be implemented some day, some way + availabilityWarn('AudioManager.requestRecordingPermissions', 'web'); + return Promise.resolve('Denied'); + } + + async checkRecordingPermissions(): Promise { + // TODO: could be implemented some day, some way + availabilityWarn('AudioManager.checkRecordingPermissions', 'web'); + return Promise.resolve('Denied'); + } + + async getDevicesInfo(): Promise { + availabilityWarn('AudioManager.getDevicesInfo', 'web'); + return Promise.resolve({ + availableInputs: [], + availableOutputs: [], + currentInputs: [], + currentOutputs: [], + }); + } +} + +export default new AudioManager(); diff --git a/packages/react-native-audio-api/prev_src/system/index.ts b/packages/react-native-audio-api/src/system/index.ts similarity index 100% rename from packages/react-native-audio-api/prev_src/system/index.ts rename to packages/react-native-audio-api/src/system/index.ts 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/prev_src/system/types.ts b/packages/react-native-audio-api/src/system/types.ts similarity index 100% rename from packages/react-native-audio-api/prev_src/system/types.ts rename to packages/react-native-audio-api/src/system/types.ts From b00db3196cf709b8ffd474266427b81270cfe6c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20S=C4=99k?= Date: Tue, 30 Sep 2025 17:08:53 +0200 Subject: [PATCH 05/18] chore: cleanup web prev --- .../prev_src/web-core/AnalyserNode.tsx | 47 ------ .../prev_src/web-core/AudioBuffer.tsx | 69 --------- .../web-core/AudioDestinationNode.tsx | 3 - .../prev_src/web-core/AudioNode.tsx | 49 ------- .../prev_src/web-core/AudioParam.tsx | 138 ------------------ .../web-core/AudioScheduledSourceNode.tsx | 43 ------ .../prev_src/web-core/BiquadFilterNode.tsx | 52 ------- .../prev_src/web-core/GainNode.tsx | 12 -- 8 files changed, 413 deletions(-) delete mode 100644 packages/react-native-audio-api/prev_src/web-core/AnalyserNode.tsx delete mode 100644 packages/react-native-audio-api/prev_src/web-core/AudioBuffer.tsx delete mode 100644 packages/react-native-audio-api/prev_src/web-core/AudioDestinationNode.tsx delete mode 100644 packages/react-native-audio-api/prev_src/web-core/AudioNode.tsx delete mode 100644 packages/react-native-audio-api/prev_src/web-core/AudioParam.tsx delete mode 100644 packages/react-native-audio-api/prev_src/web-core/AudioScheduledSourceNode.tsx delete mode 100644 packages/react-native-audio-api/prev_src/web-core/BiquadFilterNode.tsx delete mode 100644 packages/react-native-audio-api/prev_src/web-core/GainNode.tsx diff --git a/packages/react-native-audio-api/prev_src/web-core/AnalyserNode.tsx b/packages/react-native-audio-api/prev_src/web-core/AnalyserNode.tsx deleted file mode 100644 index c74b67e05..000000000 --- a/packages/react-native-audio-api/prev_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/prev_src/web-core/AudioBuffer.tsx b/packages/react-native-audio-api/prev_src/web-core/AudioBuffer.tsx deleted file mode 100644 index d007a6dd4..000000000 --- a/packages/react-native-audio-api/prev_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/prev_src/web-core/AudioDestinationNode.tsx b/packages/react-native-audio-api/prev_src/web-core/AudioDestinationNode.tsx deleted file mode 100644 index f1d850e90..000000000 --- a/packages/react-native-audio-api/prev_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/prev_src/web-core/AudioNode.tsx b/packages/react-native-audio-api/prev_src/web-core/AudioNode.tsx deleted file mode 100644 index a66113f1f..000000000 --- a/packages/react-native-audio-api/prev_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/prev_src/web-core/AudioParam.tsx b/packages/react-native-audio-api/prev_src/web-core/AudioParam.tsx deleted file mode 100644 index 341f59e55..000000000 --- a/packages/react-native-audio-api/prev_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/prev_src/web-core/AudioScheduledSourceNode.tsx b/packages/react-native-audio-api/prev_src/web-core/AudioScheduledSourceNode.tsx deleted file mode 100644 index 343d2cbf9..000000000 --- a/packages/react-native-audio-api/prev_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/prev_src/web-core/BiquadFilterNode.tsx b/packages/react-native-audio-api/prev_src/web-core/BiquadFilterNode.tsx deleted file mode 100644 index 4a8a4df21..000000000 --- a/packages/react-native-audio-api/prev_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/prev_src/web-core/GainNode.tsx b/packages/react-native-audio-api/prev_src/web-core/GainNode.tsx deleted file mode 100644 index 601de5920..000000000 --- a/packages/react-native-audio-api/prev_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); - } -} From 74d03edcd17284e77996a142d9047dd89b4bc867 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20S=C4=99k?= Date: Tue, 30 Sep 2025 17:39:50 +0200 Subject: [PATCH 06/18] feat: oscillator and periodic --- .../prev_src/core/OscillatorNode.ts | 37 --------------- .../prev_src/interfaces.ts | 20 +------- .../prev_src/web-core/OscillatorNode.tsx | 36 -------------- .../{prev_src => src}/core/PeriodicWave.ts | 2 +- .../react-native-audio-api/src/core/index.ts | 7 +++ .../src/core/sources/OscillatorNode.ts | 47 +++++++++++++++++++ .../src/types/internal/IOscillatorNode.ts | 15 ++++++ .../src/types/internal/IPeriodicWave.ts | 1 + .../src/types/internal/index.ts | 2 + .../src/types/properties.ts | 7 +++ 10 files changed, 81 insertions(+), 93 deletions(-) delete mode 100644 packages/react-native-audio-api/prev_src/core/OscillatorNode.ts delete mode 100644 packages/react-native-audio-api/prev_src/web-core/OscillatorNode.tsx rename packages/react-native-audio-api/{prev_src => src}/core/PeriodicWave.ts (77%) create mode 100644 packages/react-native-audio-api/src/core/sources/OscillatorNode.ts create mode 100644 packages/react-native-audio-api/src/types/internal/IOscillatorNode.ts create mode 100644 packages/react-native-audio-api/src/types/internal/IPeriodicWave.ts diff --git a/packages/react-native-audio-api/prev_src/core/OscillatorNode.ts b/packages/react-native-audio-api/prev_src/core/OscillatorNode.ts deleted file mode 100644 index 40248071e..000000000 --- a/packages/react-native-audio-api/prev_src/core/OscillatorNode.ts +++ /dev/null @@ -1,37 +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'; - -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); - } -} diff --git a/packages/react-native-audio-api/prev_src/interfaces.ts b/packages/react-native-audio-api/prev_src/interfaces.ts index e0a103f49..8d53b48e6 100644 --- a/packages/react-native-audio-api/prev_src/interfaces.ts +++ b/packages/react-native-audio-api/prev_src/interfaces.ts @@ -1,4 +1,4 @@ -import { ContextState, OscillatorType } from './types'; +import { ContextState } from './types'; export type ShareableWorkletCallback = ( audioBuffers: Array, @@ -57,14 +57,6 @@ export interface IOfflineAudioContext extends IBaseAudioContext { startRendering(): Promise; } -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; @@ -75,14 +67,6 @@ export interface IAudioBufferBaseSourceNode extends IAudioScheduledSourceNode { 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; } @@ -108,8 +92,6 @@ export interface IAudioBufferQueueSourceNode pause: () => void; } -export interface IPeriodicWave {} - export interface IRecorderAdapterNode extends IAudioNode {} export interface IWorkletNode extends IAudioNode {} diff --git a/packages/react-native-audio-api/prev_src/web-core/OscillatorNode.tsx b/packages/react-native-audio-api/prev_src/web-core/OscillatorNode.tsx deleted file mode 100644 index 612cdd9f6..000000000 --- a/packages/react-native-audio-api/prev_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/prev_src/core/PeriodicWave.ts b/packages/react-native-audio-api/src/core/PeriodicWave.ts similarity index 77% rename from packages/react-native-audio-api/prev_src/core/PeriodicWave.ts rename to packages/react-native-audio-api/src/core/PeriodicWave.ts index b00c4505c..0f7363892 100644 --- a/packages/react-native-audio-api/prev_src/core/PeriodicWave.ts +++ b/packages/react-native-audio-api/src/core/PeriodicWave.ts @@ -1,4 +1,4 @@ -import { IPeriodicWave } from '../interfaces'; +import type { IPeriodicWave } from '../types/internal'; export default class PeriodicWave { /** @internal */ diff --git a/packages/react-native-audio-api/src/core/index.ts b/packages/react-native-audio-api/src/core/index.ts index 352f8d9e6..45df788b8 100644 --- a/packages/react-native-audio-api/src/core/index.ts +++ b/packages/react-native-audio-api/src/core/index.ts @@ -1,7 +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/OscillatorNode.ts b/packages/react-native-audio-api/src/core/sources/OscillatorNode.ts new file mode 100644 index 000000000..5de34f1ee --- /dev/null +++ b/packages/react-native-audio-api/src/core/sources/OscillatorNode.ts @@ -0,0 +1,47 @@ +import { InvalidStateError } from '../../errors'; +import type { OscillatorType } from '../../types'; +import type { IBaseAudioContext, IOscillatorNode } from '../../types/internal'; +import AudioParam from '../AudioParam'; +import PeriodicWave from '../PeriodicWave'; + +import AudioScheduledSourceNode from './AudioScheduledSourceNode'; + +export default class OscillatorNode< + TContext extends IBaseAudioContext, + NContext extends IBaseAudioContext, + > + extends AudioScheduledSourceNode + implements IOscillatorNode +{ + readonly frequency: AudioParam; + readonly detune: AudioParam; + + constructor(context: TContext, node: IOscillatorNode) { + super(context, node); + + this.frequency = new AudioParam( + node.frequency, + context + ); + + this.detune = new AudioParam(node.detune, context); + } + + 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); + } +} diff --git a/packages/react-native-audio-api/src/types/internal/IOscillatorNode.ts b/packages/react-native-audio-api/src/types/internal/IOscillatorNode.ts new file mode 100644 index 000000000..a044addd0 --- /dev/null +++ b/packages/react-native-audio-api/src/types/internal/IOscillatorNode.ts @@ -0,0 +1,15 @@ +import { OscillatorType } from '../../types/properties'; + +import type IAudioParam from './IAudioParam'; +import type IAudioScheduledSourceNode from './IAudioScheduledSourceNode'; +import type IBaseAudioContext from './IBaseAudioContext'; +import type IPeriodicWave from './IPeriodicWave'; + +export default interface IOscillatorNode + extends IAudioScheduledSourceNode { + readonly frequency: IAudioParam; + readonly detune: IAudioParam; + type: OscillatorType; + + setPeriodicWave(periodicWave: IPeriodicWave): void; +} diff --git a/packages/react-native-audio-api/src/types/internal/IPeriodicWave.ts b/packages/react-native-audio-api/src/types/internal/IPeriodicWave.ts new file mode 100644 index 000000000..48c9f912a --- /dev/null +++ b/packages/react-native-audio-api/src/types/internal/IPeriodicWave.ts @@ -0,0 +1 @@ +export default interface IPeriodicWave {} diff --git a/packages/react-native-audio-api/src/types/internal/index.ts b/packages/react-native-audio-api/src/types/internal/index.ts index 35ba1f98d..8d8c609af 100644 --- a/packages/react-native-audio-api/src/types/internal/index.ts +++ b/packages/react-native-audio-api/src/types/internal/index.ts @@ -7,4 +7,6 @@ export { default as IAudioScheduledSourceNode } from './IAudioScheduledSourceNod export { default as IBaseAudioContext } from './IBaseAudioContext'; export { default as IBiquadFilterNode } from './IBiquadFilterNode'; export { default as IGainNode } from './IGainNode'; +export { default as IOscillatorNode } from './IOscillatorNode'; +export { default as IPeriodicWave } from './IPeriodicWave'; export { default as IStereoPannerNode } from './IStereoPannerNode'; diff --git a/packages/react-native-audio-api/src/types/properties.ts b/packages/react-native-audio-api/src/types/properties.ts index 81752176a..09cce1a77 100644 --- a/packages/react-native-audio-api/src/types/properties.ts +++ b/packages/react-native-audio-api/src/types/properties.ts @@ -12,3 +12,10 @@ export type BiquadFilterType = | 'allpass'; export type WindowType = 'blackman' | 'hann'; + +export type OscillatorType = + | 'sine' + | 'square' + | 'sawtooth' + | 'triangle' + | 'custom'; From c741e248a91df7925b2f66d4c38cc6a2109153ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20S=C4=99k?= Date: Thu, 2 Oct 2025 12:08:51 +0200 Subject: [PATCH 07/18] feat: hop on the audio thread, we have more generics to do --- .../prev_src/api.web.ts | 33 ------ .../prev_src/core/AudioBufferSourceNode.ts | 110 +++++++++--------- .../prev_src/interfaces.ts | 10 -- .../prev_src/web-core/PeriodicWave.tsx | 8 -- .../prev_src/web-core/StereoPannerNode.tsx | 12 -- .../src/core/AudioNode.ts | 5 +- .../src/core/analysis/AnalyserNode.ts | 6 +- .../src/core/analysis/AnalyserNode.web.ts | 32 ++--- .../core/destinations/AudioDestinationNode.ts | 2 +- .../src/core/effects/BiquadFilterNode.ts | 8 +- .../src/core/effects/GainNode.ts | 2 +- .../src/core/effects/StereoPannerNode.ts | 2 +- .../src/core/sources/AudioBufferSourceNode.ts | 0 .../core/sources/AudioBufferSourceNode.web.ts | 0 .../core/sources/AudioScheduledSourceNode.ts | 9 +- .../sources/AudioScheduledSourceNode.web.ts | 8 +- .../src/core/sources/OscillatorNode.ts | 12 +- .../internal/IAudioBufferBaseSourceNode.ts | 10 ++ .../types/internal/IAudioBufferSourceNode.ts | 15 +++ .../types/internal/PlaygroundToBeRemoved.ts | 91 +++++++++++++++ .../src/types/internal/index.ts | 1 + 21 files changed, 219 insertions(+), 157 deletions(-) delete mode 100644 packages/react-native-audio-api/prev_src/web-core/PeriodicWave.tsx delete mode 100644 packages/react-native-audio-api/prev_src/web-core/StereoPannerNode.tsx create mode 100644 packages/react-native-audio-api/src/core/sources/AudioBufferSourceNode.ts create mode 100644 packages/react-native-audio-api/src/core/sources/AudioBufferSourceNode.web.ts create mode 100644 packages/react-native-audio-api/src/types/internal/IAudioBufferBaseSourceNode.ts create mode 100644 packages/react-native-audio-api/src/types/internal/IAudioBufferSourceNode.ts create mode 100644 packages/react-native-audio-api/src/types/internal/PlaygroundToBeRemoved.ts diff --git a/packages/react-native-audio-api/prev_src/api.web.ts b/packages/react-native-audio-api/prev_src/api.web.ts index e47ce28ba..5f912fdd6 100644 --- a/packages/react-native-audio-api/prev_src/api.web.ts +++ b/packages/react-native-audio-api/prev_src/api.web.ts @@ -1,34 +1 @@ -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 * 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/prev_src/core/AudioBufferSourceNode.ts b/packages/react-native-audio-api/prev_src/core/AudioBufferSourceNode.ts index 7bda1a425..50efe55c1 100644 --- a/packages/react-native-audio-api/prev_src/core/AudioBufferSourceNode.ts +++ b/packages/react-native-audio-api/prev_src/core/AudioBufferSourceNode.ts @@ -1,26 +1,26 @@ -import { IAudioBufferSourceNode } from '../interfaces'; -import AudioBufferBaseSourceNode from './AudioBufferBaseSourceNode'; -import AudioBuffer from './AudioBuffer'; import { InvalidStateError, RangeError } from '../errors'; import { EventEmptyType } from '../events/types'; +import { IAudioBufferSourceNode } from '../interfaces'; +import AudioBuffer from './AudioBuffer'; +import AudioBufferBaseSourceNode from './AudioBufferBaseSourceNode'; export default class AudioBufferSourceNode extends AudioBufferBaseSourceNode { - 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 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; @@ -54,40 +54,40 @@ export default class AudioBufferSourceNode extends AudioBufferBaseSourceNode { (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 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; + // } } diff --git a/packages/react-native-audio-api/prev_src/interfaces.ts b/packages/react-native-audio-api/prev_src/interfaces.ts index 8d53b48e6..d39b529ea 100644 --- a/packages/react-native-audio-api/prev_src/interfaces.ts +++ b/packages/react-native-audio-api/prev_src/interfaces.ts @@ -58,9 +58,6 @@ export interface IOfflineAudioContext extends IBaseAudioContext { } 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 @@ -72,13 +69,6 @@ export interface IStreamerNode extends IAudioNode { } 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; } diff --git a/packages/react-native-audio-api/prev_src/web-core/PeriodicWave.tsx b/packages/react-native-audio-api/prev_src/web-core/PeriodicWave.tsx deleted file mode 100644 index bdf8979e1..000000000 --- a/packages/react-native-audio-api/prev_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/prev_src/web-core/StereoPannerNode.tsx b/packages/react-native-audio-api/prev_src/web-core/StereoPannerNode.tsx deleted file mode 100644 index 2d468a205..000000000 --- a/packages/react-native-audio-api/prev_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/core/AudioNode.ts b/packages/react-native-audio-api/src/core/AudioNode.ts index 09d848b3c..231c8f9d0 100644 --- a/packages/react-native-audio-api/src/core/AudioNode.ts +++ b/packages/react-native-audio-api/src/core/AudioNode.ts @@ -5,6 +5,7 @@ import AudioParam from './AudioParam'; export default class AudioNode< TContext extends IBaseAudioContext, NContext extends IBaseAudioContext, + TNode extends IAudioNode = IAudioNode, > implements IAudioNode { readonly context: TContext; @@ -14,9 +15,9 @@ export default class AudioNode< readonly channelCountMode: ChannelCountMode; readonly channelInterpretation: ChannelInterpretation; - protected readonly node: IAudioNode; + protected readonly node: TNode; - constructor(context: TContext, node: IAudioNode) { + constructor(context: TContext, node: TNode) { this.context = context; this.node = node; this.numberOfInputs = this.node.numberOfInputs; diff --git a/packages/react-native-audio-api/src/core/analysis/AnalyserNode.ts b/packages/react-native-audio-api/src/core/analysis/AnalyserNode.ts index ab79233d7..21b5d8a71 100644 --- a/packages/react-native-audio-api/src/core/analysis/AnalyserNode.ts +++ b/packages/react-native-audio-api/src/core/analysis/AnalyserNode.ts @@ -1,5 +1,5 @@ import type { WindowType } from '../../types'; -import type { IAnalyserNode, IBaseAudioContext } from '../../types/internal'; +import type { IBaseAudioContext } from '../../types/internal'; import AnalyserNode from './AnalyserNode.web'; export default class AnalyserNodeNative< @@ -7,10 +7,10 @@ export default class AnalyserNodeNative< NContext extends IBaseAudioContext, > extends AnalyserNode { public get window(): WindowType { - return (this.node as IAnalyserNode).window; + return this.node.window; } public set window(value: WindowType) { - (this.node as IAnalyserNode).window = value; + 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 index 046064160..dbd63b8d8 100644 --- a/packages/react-native-audio-api/src/core/analysis/AnalyserNode.web.ts +++ b/packages/react-native-audio-api/src/core/analysis/AnalyserNode.web.ts @@ -12,11 +12,11 @@ export default class AnalyserNode< TContext extends IBaseAudioContext, NContext extends IBaseAudioContext, > - extends AudioNode + extends AudioNode> implements IAnalyserNode { public get fftSize(): number { - return (this.node as IAnalyserNode).fftSize; + return this.node.fftSize; } public set fftSize(value: number) { @@ -26,25 +26,25 @@ export default class AnalyserNode< ); } - (this.node as IAnalyserNode).fftSize = value; + this.node.fftSize = value; } public get minDecibels(): number { - return (this.node as IAnalyserNode).minDecibels; + return this.node.minDecibels; } public set minDecibels(value: number) { - if (value >= (this.node as IAnalyserNode).maxDecibels) { + if (value >= this.node.maxDecibels) { throw new IndexSizeError( `The minDecibels value (${value}) must be less than maxDecibels` ); } - (this.node as IAnalyserNode).minDecibels = value; + this.node.minDecibels = value; } public get smoothingTimeConstant(): number { - return (this.node as IAnalyserNode).smoothingTimeConstant; + return this.node.smoothingTimeConstant; } public set smoothingTimeConstant(value: number) { @@ -54,21 +54,21 @@ export default class AnalyserNode< ); } - (this.node as IAnalyserNode).smoothingTimeConstant = value; + this.node.smoothingTimeConstant = value; } public get maxDecibels(): number { - return (this.node as IAnalyserNode).maxDecibels; + return this.node.maxDecibels; } public set maxDecibels(value: number) { - if (value <= (this.node as IAnalyserNode).minDecibels) { + if (value <= this.node.minDecibels) { throw new IndexSizeError( `The maxDecibels value (${value}) must be greater than minDecibels` ); } - (this.node as IAnalyserNode).maxDecibels = value; + this.node.maxDecibels = value; } public get window(): WindowType { @@ -80,22 +80,22 @@ export default class AnalyserNode< } public get frequencyBinCount(): number { - return (this.node as IAnalyserNode).frequencyBinCount; + return this.node.frequencyBinCount; } public getFloatFrequencyData(array: Float32Array): void { - (this.node as IAnalyserNode).getFloatFrequencyData(array); + this.node.getFloatFrequencyData(array); } public getByteFrequencyData(array: Uint8Array): void { - (this.node as IAnalyserNode).getByteFrequencyData(array); + this.node.getByteFrequencyData(array); } public getFloatTimeDomainData(array: Float32Array): void { - (this.node as IAnalyserNode).getFloatTimeDomainData(array); + this.node.getFloatTimeDomainData(array); } public getByteTimeDomainData(array: Uint8Array): void { - (this.node as IAnalyserNode).getByteTimeDomainData(array); + this.node.getByteTimeDomainData(array); } } diff --git a/packages/react-native-audio-api/src/core/destinations/AudioDestinationNode.ts b/packages/react-native-audio-api/src/core/destinations/AudioDestinationNode.ts index 53a3723dd..e348d14d5 100644 --- a/packages/react-native-audio-api/src/core/destinations/AudioDestinationNode.ts +++ b/packages/react-native-audio-api/src/core/destinations/AudioDestinationNode.ts @@ -8,7 +8,7 @@ export default class AudioDestinationNode< TContext extends IBaseAudioContext, NContext extends IBaseAudioContext, > - extends AudioNode + extends AudioNode> implements IAudioDestinationNode { // 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 index b40eb68e9..7643598f7 100644 --- a/packages/react-native-audio-api/src/core/effects/BiquadFilterNode.ts +++ b/packages/react-native-audio-api/src/core/effects/BiquadFilterNode.ts @@ -11,7 +11,7 @@ export default class BiquadFilterNode< TContext extends IBaseAudioContext, NContext extends IBaseAudioContext, > - extends AudioNode + extends AudioNode> implements IBiquadFilterNode { readonly frequency: AudioParam; @@ -35,11 +35,11 @@ export default class BiquadFilterNode< } public get type(): BiquadFilterType { - return (this.node as IBiquadFilterNode).type; + return this.node.type; } public set type(value: BiquadFilterType) { - (this.node as IBiquadFilterNode).type = value; + this.node.type = value; } public getFrequencyResponse( @@ -56,7 +56,7 @@ export default class BiquadFilterNode< ); } - (this.node as IBiquadFilterNode).getFrequencyResponse( + 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 index d032d059c..3d1360237 100644 --- a/packages/react-native-audio-api/src/core/effects/GainNode.ts +++ b/packages/react-native-audio-api/src/core/effects/GainNode.ts @@ -6,7 +6,7 @@ export default class GainNode< TContext extends IBaseAudioContext, NContext extends IBaseAudioContext, > - extends AudioNode + extends AudioNode> implements IGainNode { readonly gain: AudioParam; diff --git a/packages/react-native-audio-api/src/core/effects/StereoPannerNode.ts b/packages/react-native-audio-api/src/core/effects/StereoPannerNode.ts index fd4b9958b..0cff2b08e 100644 --- a/packages/react-native-audio-api/src/core/effects/StereoPannerNode.ts +++ b/packages/react-native-audio-api/src/core/effects/StereoPannerNode.ts @@ -8,7 +8,7 @@ import AudioParam from '../AudioParam'; export default class StereoPannerNode< TContext extends IBaseAudioContext, NContext extends IBaseAudioContext, -> extends AudioNode { +> extends AudioNode> { readonly pan: AudioParam; constructor(context: TContext, pan: IStereoPannerNode) { diff --git a/packages/react-native-audio-api/src/core/sources/AudioBufferSourceNode.ts b/packages/react-native-audio-api/src/core/sources/AudioBufferSourceNode.ts new file mode 100644 index 000000000..e69de29bb 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..e69de29bb diff --git a/packages/react-native-audio-api/src/core/sources/AudioScheduledSourceNode.ts b/packages/react-native-audio-api/src/core/sources/AudioScheduledSourceNode.ts index d8beab7f0..60a1bcdcb 100644 --- a/packages/react-native-audio-api/src/core/sources/AudioScheduledSourceNode.ts +++ b/packages/react-native-audio-api/src/core/sources/AudioScheduledSourceNode.ts @@ -14,7 +14,9 @@ interface INativeAudioScheduledSourceNode export default class AudioScheduledSourceNodeNative< TContext extends IBaseAudioContext, NContext extends IBaseAudioContext, -> extends AudioScheduledSourceNode { + TNode extends + INativeAudioScheduledSourceNode = INativeAudioScheduledSourceNode, +> extends AudioScheduledSourceNode { protected readonly audioEventEmitter = new AudioEventEmitter( global.AudioEventEmitter ); @@ -28,7 +30,7 @@ export default class AudioScheduledSourceNodeNative< public set onEnded(callback: OnEndedEventCallback | null) { if (!callback) { - (this.node as INativeAudioScheduledSourceNode).onEnded = '0'; + this.node.onEnded = '0'; this.onEndedSubscription?.remove(); this.onEndedSubscription = undefined; this.onEndedCallbackNative = null; @@ -41,7 +43,6 @@ export default class AudioScheduledSourceNodeNative< callback ); - (this.node as INativeAudioScheduledSourceNode).onEnded = - this.onEndedSubscription.subscriptionId; + 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 index 7519af30d..6482d82f9 100644 --- a/packages/react-native-audio-api/src/core/sources/AudioScheduledSourceNode.web.ts +++ b/packages/react-native-audio-api/src/core/sources/AudioScheduledSourceNode.web.ts @@ -11,8 +11,10 @@ export type OnEndedEventCallback = (event: OnEndedEventType) => void; export default class AudioScheduledSourceNode< TContext extends IBaseAudioContext, NContext extends IBaseAudioContext, + TNode extends + IAudioScheduledSourceNode = IAudioScheduledSourceNode, > - extends AudioNode + extends AudioNode implements IAudioScheduledSourceNode { protected hasBeenStarted: boolean = false; @@ -30,7 +32,7 @@ export default class AudioScheduledSourceNode< } this.hasBeenStarted = true; - (this.node as IAudioScheduledSourceNode).start(when); + this.node.start(when); } public stop(when: number = 0): void { @@ -46,7 +48,7 @@ export default class AudioScheduledSourceNode< ); } - (this.node as IAudioScheduledSourceNode).stop(when); + this.node.stop(when); } public get onEnded(): OnEndedEventCallback | undefined { diff --git a/packages/react-native-audio-api/src/core/sources/OscillatorNode.ts b/packages/react-native-audio-api/src/core/sources/OscillatorNode.ts index 5de34f1ee..9a4e2b603 100644 --- a/packages/react-native-audio-api/src/core/sources/OscillatorNode.ts +++ b/packages/react-native-audio-api/src/core/sources/OscillatorNode.ts @@ -10,7 +10,11 @@ export default class OscillatorNode< TContext extends IBaseAudioContext, NContext extends IBaseAudioContext, > - extends AudioScheduledSourceNode + extends AudioScheduledSourceNode< + TContext, + NContext, + IOscillatorNode + > implements IOscillatorNode { readonly frequency: AudioParam; @@ -28,7 +32,7 @@ export default class OscillatorNode< } public get type(): OscillatorType { - return (this.node as IOscillatorNode).type; + return this.node.type; } public set type(value: OscillatorType) { @@ -38,10 +42,10 @@ export default class OscillatorNode< ); } - (this.node as IOscillatorNode).type = value; + this.node.type = value; } public setPeriodicWave(wave: PeriodicWave): void { - (this.node as IOscillatorNode).setPeriodicWave(wave.periodicWave); + this.node.setPeriodicWave(wave.periodicWave); } } diff --git a/packages/react-native-audio-api/src/types/internal/IAudioBufferBaseSourceNode.ts b/packages/react-native-audio-api/src/types/internal/IAudioBufferBaseSourceNode.ts new file mode 100644 index 000000000..5e0949923 --- /dev/null +++ b/packages/react-native-audio-api/src/types/internal/IAudioBufferBaseSourceNode.ts @@ -0,0 +1,10 @@ +import type IAudioParam from './IAudioParam'; +import type IAudioScheduledSourceNode from './IAudioScheduledSourceNode'; +import type IBaseAudioContext from './IBaseAudioContext'; + +export default interface IAudioBufferBaseSourceNode< + TContext extends IBaseAudioContext, +> extends IAudioScheduledSourceNode { + detune: IAudioParam; + playbackRate: IAudioParam; +} diff --git a/packages/react-native-audio-api/src/types/internal/IAudioBufferSourceNode.ts b/packages/react-native-audio-api/src/types/internal/IAudioBufferSourceNode.ts new file mode 100644 index 000000000..2f53cacb8 --- /dev/null +++ b/packages/react-native-audio-api/src/types/internal/IAudioBufferSourceNode.ts @@ -0,0 +1,15 @@ +import type IAudioBuffer from './IAudioBuffer'; +import type IAudioBufferBaseSourceNode from './IAudioBufferBaseSourceNode'; +import type IBaseAudioContext from './IBaseAudioContext'; + +export default interface IAudioBufferSourceNode< + TContext extends IBaseAudioContext, +> extends IAudioBufferBaseSourceNode { + buffer: IAudioBuffer | null; + loop: boolean; + loopSkip: boolean; + loopStart: number; + loopEnd: number; + + start: (when?: number, offset?: number, duration?: number) => void; +} diff --git a/packages/react-native-audio-api/src/types/internal/PlaygroundToBeRemoved.ts b/packages/react-native-audio-api/src/types/internal/PlaygroundToBeRemoved.ts new file mode 100644 index 000000000..50a90b7ab --- /dev/null +++ b/packages/react-native-audio-api/src/types/internal/PlaygroundToBeRemoved.ts @@ -0,0 +1,91 @@ +// TS don't complain +interface IUniversalAudioNode {} + +// src/types/internal/IAudioSekuNode.ts - interface definitions + +export interface IUniversalAudioSekuNode extends IUniversalAudioNode { + readonly knotsLength: number; +} + +export interface IRNNativeAudioSekuNode extends IUniversalAudioSekuNode { + readonly knots: Array; + setKnots(knots: Array): void; +} + +export interface IWebNativeAudioSekuNode extends IUniversalAudioSekuNode { + readonly knots: Uint8Array; + setKnots(knots: Uint8Array): void; +} + +export type INativeAudioSekuNode = + | IRNNativeAudioSekuNode + | IWebNativeAudioSekuNode; + +export interface IRNAudioSekuNode extends IUniversalAudioSekuNode { + readonly knots: Array; + setKnots(knots: Array): void; +} + +// src/core/effects/BaseAudioSekuNode.ts - base class definition + +export class BaseAudioSekuNode implements IUniversalAudioSekuNode { + protected readonly node: INativeAudioSekuNode; + + constructor(node: INativeAudioSekuNode) { + this.node = node; + } + + public get knotsLength(): number { + return this.node.knotsLength; + } +} + +// src/core/effects/AudioSekuNode.ts - native (RN default - but this doesn't matter) class definition + +// NOTE: RN prefix is for single-file example/use-case +export class RNAudioSekuNode + extends BaseAudioSekuNode + implements IRNAudioSekuNode +{ + public get knots(): Array { + return (this.node as IRNNativeAudioSekuNode).knots; + } + + public setKnots(knots: Array): void { + (this.node as IRNNativeAudioSekuNode).setKnots(knots); + } +} + +// src/core/effects/AudioSekuNode.web.ts - web class definition + +// NOTE: Web prefix is for single-file example/use-case +export class WebAudioSekuNode + extends BaseAudioSekuNode + implements IRNAudioSekuNode +{ + public get knots(): Array { + return Array.from((this.node as IWebNativeAudioSekuNode).knots); + } + + public setKnots(knots: Array): void { + (this.node as IWebNativeAudioSekuNode).setKnots(new Uint8Array(knots)); + } +} + +interface BaseNode { + readonly blabla: string; +} + +interface Extension { + readonly extension: string; +} + +export class Example implements BaseNode, Extension { + public get blabla(): string { + return 'blabla'; + } + + public get extension(): string { + return 'extension'; + } +} diff --git a/packages/react-native-audio-api/src/types/internal/index.ts b/packages/react-native-audio-api/src/types/internal/index.ts index 8d8c609af..0c2ee5c7f 100644 --- a/packages/react-native-audio-api/src/types/internal/index.ts +++ b/packages/react-native-audio-api/src/types/internal/index.ts @@ -1,5 +1,6 @@ export { default as IAnalyserNode } from './IAnalyserNode'; export { default as IAudioBuffer } from './IAudioBuffer'; +export { default as IAudioBufferBaseSourceNode } from './IAudioBufferBaseSourceNode'; export { default as IAudioDestinationNode } from './IAudioDestinationNode'; export { default as IAudioNode } from './IAudioNode'; export { default as IAudioParam } from './IAudioParam'; From 2f495ef87dbd15ca0fd82f996b1904d067018133 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20S=C4=99k?= Date: Thu, 2 Oct 2025 13:06:46 +0200 Subject: [PATCH 08/18] feat: generic generics --- .../IGenericAudioBuffer.ts} | 2 +- .../generics/IGenericAudioDestinationNode.ts | 9 ++ .../src/types/generics/IGenericAudioNode.ts | 26 ++++++ .../src/types/generics/IGenericAudioParam.ts | 29 ++++++ .../generics/IGenericBaseAudioContext.ts | 2 + .../generics/IGenericBiquadFilterNode.ts | 20 ++++ .../src/types/generics/IGenericGainNode.ts | 9 ++ .../types/generics/IGenericOscillatorNode.ts | 15 +++ .../types/generics/IGenericPeriodicWave.ts | 1 + .../generics/IGenericStereoPannerNode.ts | 9 ++ .../src/types/generics/index.ts | 22 +++++ .../src/types/interfaces/index.ts | 7 ++ .../types/internal/IAudioDestinationNode.ts | 9 -- .../src/types/internal/IAudioNode.ts | 21 ----- .../src/types/internal/IAudioParam.ts | 23 ----- .../src/types/internal/IBaseAudioContext.ts | 1 - .../src/types/internal/IBiquadFilterNode.ts | 19 ---- .../src/types/internal/IGainNode.ts | 8 -- .../src/types/internal/IPeriodicWave.ts | 1 - .../src/types/internal/IStereoPannerNode.ts | 8 -- .../types/internal/PlaygroundToBeRemoved.ts | 91 ------------------- .../src/types/internal/index.ts | 13 --- 22 files changed, 150 insertions(+), 195 deletions(-) rename packages/react-native-audio-api/src/types/{internal/IAudioBuffer.ts => generics/IGenericAudioBuffer.ts} (89%) create mode 100644 packages/react-native-audio-api/src/types/generics/IGenericAudioDestinationNode.ts create mode 100644 packages/react-native-audio-api/src/types/generics/IGenericAudioNode.ts create mode 100644 packages/react-native-audio-api/src/types/generics/IGenericAudioParam.ts create mode 100644 packages/react-native-audio-api/src/types/generics/IGenericBaseAudioContext.ts create mode 100644 packages/react-native-audio-api/src/types/generics/IGenericBiquadFilterNode.ts create mode 100644 packages/react-native-audio-api/src/types/generics/IGenericGainNode.ts create mode 100644 packages/react-native-audio-api/src/types/generics/IGenericOscillatorNode.ts create mode 100644 packages/react-native-audio-api/src/types/generics/IGenericPeriodicWave.ts create mode 100644 packages/react-native-audio-api/src/types/generics/IGenericStereoPannerNode.ts create mode 100644 packages/react-native-audio-api/src/types/generics/index.ts create mode 100644 packages/react-native-audio-api/src/types/interfaces/index.ts delete mode 100644 packages/react-native-audio-api/src/types/internal/IAudioDestinationNode.ts delete mode 100644 packages/react-native-audio-api/src/types/internal/IAudioNode.ts delete mode 100644 packages/react-native-audio-api/src/types/internal/IAudioParam.ts delete mode 100644 packages/react-native-audio-api/src/types/internal/IBaseAudioContext.ts delete mode 100644 packages/react-native-audio-api/src/types/internal/IBiquadFilterNode.ts delete mode 100644 packages/react-native-audio-api/src/types/internal/IGainNode.ts delete mode 100644 packages/react-native-audio-api/src/types/internal/IPeriodicWave.ts delete mode 100644 packages/react-native-audio-api/src/types/internal/IStereoPannerNode.ts delete mode 100644 packages/react-native-audio-api/src/types/internal/PlaygroundToBeRemoved.ts delete mode 100644 packages/react-native-audio-api/src/types/internal/index.ts diff --git a/packages/react-native-audio-api/src/types/internal/IAudioBuffer.ts b/packages/react-native-audio-api/src/types/generics/IGenericAudioBuffer.ts similarity index 89% rename from packages/react-native-audio-api/src/types/internal/IAudioBuffer.ts rename to packages/react-native-audio-api/src/types/generics/IGenericAudioBuffer.ts index ed6ef6d2b..620bc9653 100644 --- a/packages/react-native-audio-api/src/types/internal/IAudioBuffer.ts +++ b/packages/react-native-audio-api/src/types/generics/IGenericAudioBuffer.ts @@ -1,4 +1,4 @@ -export default interface IAudioBuffer { +export default interface IGenericAudioBuffer { readonly length: number; readonly duration: number; readonly sampleRate: number; 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..f5a8af83f --- /dev/null +++ b/packages/react-native-audio-api/src/types/generics/IGenericAudioNode.ts @@ -0,0 +1,26 @@ +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..c8dc10133 --- /dev/null +++ b/packages/react-native-audio-api/src/types/generics/IGenericAudioParam.ts @@ -0,0 +1,29 @@ +export default interface IGenericAudioParam { + 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/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..595f8698c --- /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..6666c3d72 --- /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..a044addd0 --- /dev/null +++ b/packages/react-native-audio-api/src/types/generics/IGenericOscillatorNode.ts @@ -0,0 +1,15 @@ +import { OscillatorType } from '../../types/properties'; + +import type IAudioParam from './IAudioParam'; +import type IAudioScheduledSourceNode from './IAudioScheduledSourceNode'; +import type IBaseAudioContext from './IBaseAudioContext'; +import type IPeriodicWave from './IPeriodicWave'; + +export default interface IOscillatorNode + extends IAudioScheduledSourceNode { + readonly frequency: IAudioParam; + readonly detune: IAudioParam; + type: OscillatorType; + + setPeriodicWave(periodicWave: IPeriodicWave): 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..fbb645b5c --- /dev/null +++ b/packages/react-native-audio-api/src/types/generics/IGenericPeriodicWave.ts @@ -0,0 +1 @@ +export default interface IGenericPeriodicWave {} 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..7e884e760 --- /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..f1912cb24 --- /dev/null +++ b/packages/react-native-audio-api/src/types/generics/index.ts @@ -0,0 +1,22 @@ +/** + * 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 IGenericBiquadFilterNode } from './IGenericBiquadFilterNode'; +export type { default as IGenericGainNode } from './IGenericGainNode'; +export type { default as IGenericPeriodicWave } from './IGenericPeriodicWave'; +export type { default as IGenericStereoPannerNode } from './IGenericStereoPannerNode'; 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..0a5101ddd --- /dev/null +++ b/packages/react-native-audio-api/src/types/interfaces/index.ts @@ -0,0 +1,7 @@ +/** + * 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. + */ diff --git a/packages/react-native-audio-api/src/types/internal/IAudioDestinationNode.ts b/packages/react-native-audio-api/src/types/internal/IAudioDestinationNode.ts deleted file mode 100644 index 808eea015..000000000 --- a/packages/react-native-audio-api/src/types/internal/IAudioDestinationNode.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type IAudioNode from './IAudioNode'; -import type IBaseAudioContext from './IBaseAudioContext'; - -export default interface IAudioDestinationNode< - TContext extends IBaseAudioContext, -> extends IAudioNode { - // TODO: implement on native side - // readonly maxChannelCount: number; -} diff --git a/packages/react-native-audio-api/src/types/internal/IAudioNode.ts b/packages/react-native-audio-api/src/types/internal/IAudioNode.ts deleted file mode 100644 index 9714362d9..000000000 --- a/packages/react-native-audio-api/src/types/internal/IAudioNode.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type IAudioParam from './IAudioParam'; -import type IBaseAudioContext from './IBaseAudioContext'; - -export default interface IAudioNode { - readonly context: TContext; - readonly numberOfInputs: number; - readonly numberOfOutputs: number; - readonly channelCount: number; - readonly channelCountMode: ChannelCountMode; - readonly channelInterpretation: ChannelInterpretation; - - connect>( - destination: D - ): D; - connect(destination: IAudioParam): void; - - disconnect>( - destination?: D - ): void; - disconnect(destination?: IAudioParam): void; -} diff --git a/packages/react-native-audio-api/src/types/internal/IAudioParam.ts b/packages/react-native-audio-api/src/types/internal/IAudioParam.ts deleted file mode 100644 index c8879329e..000000000 --- a/packages/react-native-audio-api/src/types/internal/IAudioParam.ts +++ /dev/null @@ -1,23 +0,0 @@ -export default interface IAudioParam { - readonly defaultValue: number; - readonly minValue: number; - readonly maxValue: number; - - value: number; - - cancelAndHoldAtTime: (cancelTime: number) => IAudioParam; - cancelScheduledValues: (cancelTime: number) => IAudioParam; - exponentialRampToValueAtTime: (value: number, endTime: number) => IAudioParam; - linearRampToValueAtTime: (value: number, endTime: number) => IAudioParam; - setTargetAtTime: ( - target: number, - startTime: number, - timeConstant: number - ) => IAudioParam; - setValueAtTime: (value: number, startTime: number) => IAudioParam; - setValueCurveAtTime: ( - values: Float32Array, - startTime: number, - duration: number - ) => IAudioParam; -} diff --git a/packages/react-native-audio-api/src/types/internal/IBaseAudioContext.ts b/packages/react-native-audio-api/src/types/internal/IBaseAudioContext.ts deleted file mode 100644 index 9d7e4c699..000000000 --- a/packages/react-native-audio-api/src/types/internal/IBaseAudioContext.ts +++ /dev/null @@ -1 +0,0 @@ -export default interface IBaseAudioContext {} diff --git a/packages/react-native-audio-api/src/types/internal/IBiquadFilterNode.ts b/packages/react-native-audio-api/src/types/internal/IBiquadFilterNode.ts deleted file mode 100644 index c5c00dab3..000000000 --- a/packages/react-native-audio-api/src/types/internal/IBiquadFilterNode.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { BiquadFilterType } from '../properties'; -import type IAudioNode from './IAudioNode'; -import type IAudioParam from './IAudioParam'; -import type IBaseAudioContext from './IBaseAudioContext'; - -export default interface IStereoPannerNode - extends IAudioNode { - readonly frequency: IAudioParam; - readonly detune: IAudioParam; - readonly Q: IAudioParam; - readonly gain: IAudioParam; - type: BiquadFilterType; - - getFrequencyResponse( - frequencyArray: Float32Array, - magResponseOutput: Float32Array, - phaseResponseOutput: Float32Array - ): void; -} diff --git a/packages/react-native-audio-api/src/types/internal/IGainNode.ts b/packages/react-native-audio-api/src/types/internal/IGainNode.ts deleted file mode 100644 index 72cd2577c..000000000 --- a/packages/react-native-audio-api/src/types/internal/IGainNode.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type IAudioNode from './IAudioNode'; -import type IAudioParam from './IAudioParam'; -import type IBaseAudioContext from './IBaseAudioContext'; - -export default interface IGainNode - extends IAudioNode { - readonly gain: IAudioParam; -} diff --git a/packages/react-native-audio-api/src/types/internal/IPeriodicWave.ts b/packages/react-native-audio-api/src/types/internal/IPeriodicWave.ts deleted file mode 100644 index 48c9f912a..000000000 --- a/packages/react-native-audio-api/src/types/internal/IPeriodicWave.ts +++ /dev/null @@ -1 +0,0 @@ -export default interface IPeriodicWave {} diff --git a/packages/react-native-audio-api/src/types/internal/IStereoPannerNode.ts b/packages/react-native-audio-api/src/types/internal/IStereoPannerNode.ts deleted file mode 100644 index dcf7d6a12..000000000 --- a/packages/react-native-audio-api/src/types/internal/IStereoPannerNode.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type IAudioNode from './IAudioNode'; -import type IAudioParam from './IAudioParam'; -import type IBaseAudioContext from './IBaseAudioContext'; - -export default interface IStereoPannerNode - extends IAudioNode { - readonly pan: IAudioParam; -} diff --git a/packages/react-native-audio-api/src/types/internal/PlaygroundToBeRemoved.ts b/packages/react-native-audio-api/src/types/internal/PlaygroundToBeRemoved.ts deleted file mode 100644 index 50a90b7ab..000000000 --- a/packages/react-native-audio-api/src/types/internal/PlaygroundToBeRemoved.ts +++ /dev/null @@ -1,91 +0,0 @@ -// TS don't complain -interface IUniversalAudioNode {} - -// src/types/internal/IAudioSekuNode.ts - interface definitions - -export interface IUniversalAudioSekuNode extends IUniversalAudioNode { - readonly knotsLength: number; -} - -export interface IRNNativeAudioSekuNode extends IUniversalAudioSekuNode { - readonly knots: Array; - setKnots(knots: Array): void; -} - -export interface IWebNativeAudioSekuNode extends IUniversalAudioSekuNode { - readonly knots: Uint8Array; - setKnots(knots: Uint8Array): void; -} - -export type INativeAudioSekuNode = - | IRNNativeAudioSekuNode - | IWebNativeAudioSekuNode; - -export interface IRNAudioSekuNode extends IUniversalAudioSekuNode { - readonly knots: Array; - setKnots(knots: Array): void; -} - -// src/core/effects/BaseAudioSekuNode.ts - base class definition - -export class BaseAudioSekuNode implements IUniversalAudioSekuNode { - protected readonly node: INativeAudioSekuNode; - - constructor(node: INativeAudioSekuNode) { - this.node = node; - } - - public get knotsLength(): number { - return this.node.knotsLength; - } -} - -// src/core/effects/AudioSekuNode.ts - native (RN default - but this doesn't matter) class definition - -// NOTE: RN prefix is for single-file example/use-case -export class RNAudioSekuNode - extends BaseAudioSekuNode - implements IRNAudioSekuNode -{ - public get knots(): Array { - return (this.node as IRNNativeAudioSekuNode).knots; - } - - public setKnots(knots: Array): void { - (this.node as IRNNativeAudioSekuNode).setKnots(knots); - } -} - -// src/core/effects/AudioSekuNode.web.ts - web class definition - -// NOTE: Web prefix is for single-file example/use-case -export class WebAudioSekuNode - extends BaseAudioSekuNode - implements IRNAudioSekuNode -{ - public get knots(): Array { - return Array.from((this.node as IWebNativeAudioSekuNode).knots); - } - - public setKnots(knots: Array): void { - (this.node as IWebNativeAudioSekuNode).setKnots(new Uint8Array(knots)); - } -} - -interface BaseNode { - readonly blabla: string; -} - -interface Extension { - readonly extension: string; -} - -export class Example implements BaseNode, Extension { - public get blabla(): string { - return 'blabla'; - } - - public get extension(): string { - return 'extension'; - } -} diff --git a/packages/react-native-audio-api/src/types/internal/index.ts b/packages/react-native-audio-api/src/types/internal/index.ts deleted file mode 100644 index 0c2ee5c7f..000000000 --- a/packages/react-native-audio-api/src/types/internal/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -export { default as IAnalyserNode } from './IAnalyserNode'; -export { default as IAudioBuffer } from './IAudioBuffer'; -export { default as IAudioBufferBaseSourceNode } from './IAudioBufferBaseSourceNode'; -export { default as IAudioDestinationNode } from './IAudioDestinationNode'; -export { default as IAudioNode } from './IAudioNode'; -export { default as IAudioParam } from './IAudioParam'; -export { default as IAudioScheduledSourceNode } from './IAudioScheduledSourceNode'; -export { default as IBaseAudioContext } from './IBaseAudioContext'; -export { default as IBiquadFilterNode } from './IBiquadFilterNode'; -export { default as IGainNode } from './IGainNode'; -export { default as IOscillatorNode } from './IOscillatorNode'; -export { default as IPeriodicWave } from './IPeriodicWave'; -export { default as IStereoPannerNode } from './IStereoPannerNode'; From affdc53b78dd1d322e26f786804ba9f1303268fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20S=C4=99k?= Date: Thu, 2 Oct 2025 14:01:54 +0200 Subject: [PATCH 09/18] feat: i heard you like generics, so i've put generic inside a generic and wrapped it with a generic --- .../src/core/AudioBuffer.ts | 8 +- .../src/core/AudioNode.ts | 33 +++-- .../src/core/AudioParam.ts | 15 ++- .../src/core/PeriodicWave.ts | 15 ++- .../src/core/analysis/AnalyserNode.ts | 19 ++- .../src/core/analysis/AnalyserNode.web.ts | 94 +------------- .../src/core/analysis/BaseAnalyserNode.ts | 118 ++++++++++++++++++ .../core/destinations/AudioDestinationNode.ts | 14 +-- .../src/core/effects/BiquadFilterNode.ts | 19 +-- .../src/core/effects/GainNode.ts | 15 ++- .../src/core/effects/StereoPannerNode.ts | 14 +-- .../src/types/generics/IGenericAudioParam.ts | 23 ++-- .../types/generics/IGenericPeriodicWave.ts | 11 +- .../src/types/generics/index.ts | 1 + .../{internal => interfaces}/IAnalyserNode.ts | 8 +- .../src/types/interfaces/index.ts | 1 + 16 files changed, 248 insertions(+), 160 deletions(-) create mode 100644 packages/react-native-audio-api/src/core/analysis/BaseAnalyserNode.ts rename packages/react-native-audio-api/src/types/{internal => interfaces}/IAnalyserNode.ts (67%) diff --git a/packages/react-native-audio-api/src/core/AudioBuffer.ts b/packages/react-native-audio-api/src/core/AudioBuffer.ts index e5564bd5e..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 { IndexSizeError } from '../errors'; -import type { IAudioBuffer } from '../types/internal'; +import type { IGenericAudioBuffer } from '../types/generics'; -export default class AudioBuffer implements IAudioBuffer { +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; diff --git a/packages/react-native-audio-api/src/core/AudioNode.ts b/packages/react-native-audio-api/src/core/AudioNode.ts index 231c8f9d0..81e063005 100644 --- a/packages/react-native-audio-api/src/core/AudioNode.ts +++ b/packages/react-native-audio-api/src/core/AudioNode.ts @@ -1,12 +1,15 @@ import { ChannelCountMode, ChannelInterpretation } from '../types'; -import type { IAudioNode, IBaseAudioContext } from '../types/internal'; +import type { + IGenericAudioNode, + IGenericBaseAudioContext, +} from '../types/generics'; import AudioParam from './AudioParam'; export default class AudioNode< - TContext extends IBaseAudioContext, - NContext extends IBaseAudioContext, - TNode extends IAudioNode = IAudioNode, -> implements IAudioNode + TContext extends IGenericBaseAudioContext, + NContext extends IGenericBaseAudioContext, + TNode extends IGenericAudioNode = IGenericAudioNode, +> implements IGenericAudioNode { readonly context: TContext; readonly numberOfInputs: number; @@ -27,14 +30,15 @@ export default class AudioNode< this.channelInterpretation = this.node.channelInterpretation; } - public connect( - destination: AudioNode - ): AudioNode; + public connect>( + destination: ONode + ): ONode; public connect(destination: AudioParam): void; - public connect( - destination: AudioNode | AudioParam - ): AudioNode | void { + + public connect>( + destination: ONode | AudioParam + ): ONode | void { if (this.context !== destination.context) { throw new Error( 'Source and destination are from different BaseAudioContexts' @@ -51,8 +55,13 @@ export default class AudioNode< } public disconnect(): void; - public disconnect(destination: AudioNode): void; + + public disconnect>( + destination: ONode + ): void; + public disconnect(destination: AudioParam): void; + public disconnect( destination?: AudioNode | AudioParam ): void { diff --git a/packages/react-native-audio-api/src/core/AudioParam.ts b/packages/react-native-audio-api/src/core/AudioParam.ts index 5b6b8a1ef..829d3bc5b 100644 --- a/packages/react-native-audio-api/src/core/AudioParam.ts +++ b/packages/react-native-audio-api/src/core/AudioParam.ts @@ -1,18 +1,21 @@ import { InvalidStateError } from '../errors'; -import { IAudioParam, IBaseAudioContext } from '../types/internal'; +import { + IGenericAudioParam, + IGenericBaseAudioContext, +} from '../types/generics'; export default class AudioParam< - TContext extends IBaseAudioContext, - NContext extends IBaseAudioContext, -> implements IAudioParam + TContext extends IGenericBaseAudioContext, + NContext extends IGenericBaseAudioContext, +> implements IGenericAudioParam { readonly defaultValue: number; readonly minValue: number; readonly maxValue: number; - readonly param: IAudioParam; // IAudioParam + readonly param: IGenericAudioParam; readonly context: TContext; - constructor(param: IAudioParam, context: TContext) { + constructor(param: IGenericAudioParam, context: TContext) { this.param = param; this.context = context; this.defaultValue = param.defaultValue; diff --git a/packages/react-native-audio-api/src/core/PeriodicWave.ts b/packages/react-native-audio-api/src/core/PeriodicWave.ts index 0f7363892..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 type { IPeriodicWave } from '../types/internal'; +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/analysis/AnalyserNode.ts b/packages/react-native-audio-api/src/core/analysis/AnalyserNode.ts index 21b5d8a71..6b9add1e1 100644 --- a/packages/react-native-audio-api/src/core/analysis/AnalyserNode.ts +++ b/packages/react-native-audio-api/src/core/analysis/AnalyserNode.ts @@ -1,11 +1,20 @@ import type { WindowType } from '../../types'; -import type { IBaseAudioContext } from '../../types/internal'; -import AnalyserNode from './AnalyserNode.web'; +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 IBaseAudioContext, - NContext extends IBaseAudioContext, -> extends AnalyserNode { + TContext extends IGenericBaseAudioContext, +> extends BaseAnalyserNode { public get window(): WindowType { return this.node.window; } 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 index dbd63b8d8..16b0f4724 100644 --- a/packages/react-native-audio-api/src/core/analysis/AnalyserNode.web.ts +++ b/packages/react-native-audio-api/src/core/analysis/AnalyserNode.web.ts @@ -1,76 +1,14 @@ -import { IndexSizeError } from '../../errors'; import type { WindowType } from '../../types'; -import type { IAnalyserNode, IBaseAudioContext } from '../../types/internal'; +import type { IGenericBaseAudioContext } from '../../types/generics'; import { availabilityWarn } from '../../utils'; -import AudioNode from '../AudioNode'; +import BaseAnalyserNode from './BaseAnalyserNode'; -const allowedFFTSize = [ - 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, -]; +type NativeAnalyserNode = globalThis.AnalyserNode; +type NativeAudioContext = globalThis.BaseAudioContext; export default class AnalyserNode< - TContext extends IBaseAudioContext, - NContext extends IBaseAudioContext, - > - 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; - } - + TContext extends IGenericBaseAudioContext, +> extends BaseAnalyserNode { public get window(): WindowType { return 'blackman'; } @@ -78,24 +16,4 @@ export default class AnalyserNode< public set window(_value: WindowType) { availabilityWarn('window', 'web', '/'); } - - 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); - } } 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 index e348d14d5..331b12a50 100644 --- a/packages/react-native-audio-api/src/core/destinations/AudioDestinationNode.ts +++ b/packages/react-native-audio-api/src/core/destinations/AudioDestinationNode.ts @@ -1,15 +1,15 @@ import type { - IAudioDestinationNode, - IBaseAudioContext, -} from '../../types/internal'; + IGenericAudioDestinationNode, + IGenericBaseAudioContext, +} from '../../types/generics'; import AudioNode from '../AudioNode'; export default class AudioDestinationNode< - TContext extends IBaseAudioContext, - NContext extends IBaseAudioContext, + TContext extends IGenericBaseAudioContext, + NContext extends IGenericBaseAudioContext, > - extends AudioNode> - implements IAudioDestinationNode { + 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 index 7643598f7..ebcb23731 100644 --- a/packages/react-native-audio-api/src/core/effects/BiquadFilterNode.ts +++ b/packages/react-native-audio-api/src/core/effects/BiquadFilterNode.ts @@ -1,25 +1,28 @@ import { InvalidAccessError } from '../../errors'; import { BiquadFilterType } from '../../types'; import type { - IBaseAudioContext, - IBiquadFilterNode, -} from '../../types/internal'; + IGenericBaseAudioContext, + IGenericBiquadFilterNode, +} from '../../types/generics'; import AudioNode from '../AudioNode'; import AudioParam from '../AudioParam'; export default class BiquadFilterNode< - TContext extends IBaseAudioContext, - NContext extends IBaseAudioContext, + TContext extends IGenericBaseAudioContext, + NContext extends IGenericBaseAudioContext, > - extends AudioNode> - implements IBiquadFilterNode + extends AudioNode> + implements IGenericBiquadFilterNode { readonly frequency: AudioParam; readonly detune: AudioParam; readonly Q: AudioParam; readonly gain: AudioParam; - constructor(context: TContext, biquadFilter: IBiquadFilterNode) { + constructor( + context: TContext, + biquadFilter: IGenericBiquadFilterNode + ) { super(context, biquadFilter); this.Q = new AudioParam(biquadFilter.Q, context); diff --git a/packages/react-native-audio-api/src/core/effects/GainNode.ts b/packages/react-native-audio-api/src/core/effects/GainNode.ts index 3d1360237..44583ff19 100644 --- a/packages/react-native-audio-api/src/core/effects/GainNode.ts +++ b/packages/react-native-audio-api/src/core/effects/GainNode.ts @@ -1,17 +1,20 @@ -import type { IBaseAudioContext, IGainNode } from '../../types/internal'; +import type { + IGenericBaseAudioContext, + IGenericGainNode, +} from '../../types/generics'; import AudioNode from '../AudioNode'; import AudioParam from '../AudioParam'; export default class GainNode< - TContext extends IBaseAudioContext, - NContext extends IBaseAudioContext, + TContext extends IGenericBaseAudioContext, + NContext extends IGenericBaseAudioContext, > - extends AudioNode> - implements IGainNode + extends AudioNode> + implements IGenericGainNode { readonly gain: AudioParam; - constructor(context: TContext, gain: IGainNode) { + 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 index 0cff2b08e..e1123837d 100644 --- a/packages/react-native-audio-api/src/core/effects/StereoPannerNode.ts +++ b/packages/react-native-audio-api/src/core/effects/StereoPannerNode.ts @@ -1,17 +1,17 @@ import type { - IBaseAudioContext, - IStereoPannerNode, -} from '../../types/internal'; + IGenericBaseAudioContext, + IGenericStereoPannerNode, +} from '../../types/generics'; import AudioNode from '../AudioNode'; import AudioParam from '../AudioParam'; export default class StereoPannerNode< - TContext extends IBaseAudioContext, - NContext extends IBaseAudioContext, -> extends AudioNode> { + TContext extends IGenericBaseAudioContext, + NContext extends IGenericBaseAudioContext, +> extends AudioNode> { readonly pan: AudioParam; - constructor(context: TContext, pan: IStereoPannerNode) { + constructor(context: TContext, pan: IGenericStereoPannerNode) { super(context, pan); this.pan = new AudioParam(pan.pan, context); } diff --git a/packages/react-native-audio-api/src/types/generics/IGenericAudioParam.ts b/packages/react-native-audio-api/src/types/generics/IGenericAudioParam.ts index c8dc10133..0c4b5187b 100644 --- a/packages/react-native-audio-api/src/types/generics/IGenericAudioParam.ts +++ b/packages/react-native-audio-api/src/types/generics/IGenericAudioParam.ts @@ -1,29 +1,36 @@ -export default interface IGenericAudioParam { +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; + cancelAndHoldAtTime: (cancelTime: number) => IGenericAudioParam; + cancelScheduledValues: (cancelTime: number) => IGenericAudioParam; exponentialRampToValueAtTime: ( value: number, endTime: number - ) => IGenericAudioParam; + ) => IGenericAudioParam; linearRampToValueAtTime: ( value: number, endTime: number - ) => IGenericAudioParam; + ) => IGenericAudioParam; setTargetAtTime: ( target: number, startTime: number, timeConstant: number - ) => IGenericAudioParam; - setValueAtTime: (value: number, startTime: number) => IGenericAudioParam; + ) => IGenericAudioParam; + setValueAtTime: ( + value: number, + startTime: number + ) => IGenericAudioParam; setValueCurveAtTime: ( values: Float32Array, startTime: number, duration: number - ) => IGenericAudioParam; + ) => IGenericAudioParam; } diff --git a/packages/react-native-audio-api/src/types/generics/IGenericPeriodicWave.ts b/packages/react-native-audio-api/src/types/generics/IGenericPeriodicWave.ts index fbb645b5c..aeb77326d 100644 --- a/packages/react-native-audio-api/src/types/generics/IGenericPeriodicWave.ts +++ b/packages/react-native-audio-api/src/types/generics/IGenericPeriodicWave.ts @@ -1 +1,10 @@ -export default interface IGenericPeriodicWave {} +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/index.ts b/packages/react-native-audio-api/src/types/generics/index.ts index f1912cb24..2099cdc92 100644 --- a/packages/react-native-audio-api/src/types/generics/index.ts +++ b/packages/react-native-audio-api/src/types/generics/index.ts @@ -16,6 +16,7 @@ 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 IGenericBaseAudioContext } from './IGenericBaseAudioContext'; export type { default as IGenericBiquadFilterNode } from './IGenericBiquadFilterNode'; export type { default as IGenericGainNode } from './IGenericGainNode'; export type { default as IGenericPeriodicWave } from './IGenericPeriodicWave'; diff --git a/packages/react-native-audio-api/src/types/internal/IAnalyserNode.ts b/packages/react-native-audio-api/src/types/interfaces/IAnalyserNode.ts similarity index 67% rename from packages/react-native-audio-api/src/types/internal/IAnalyserNode.ts rename to packages/react-native-audio-api/src/types/interfaces/IAnalyserNode.ts index e0c119afe..2fa1375b0 100644 --- a/packages/react-native-audio-api/src/types/internal/IAnalyserNode.ts +++ b/packages/react-native-audio-api/src/types/interfaces/IAnalyserNode.ts @@ -1,9 +1,9 @@ +import type { IGenericAudioNode, IGenericBaseAudioContext } from '../generics'; import type { WindowType } from '../properties'; -import type IAudioNode from './IAudioNode'; -import type IBaseAudioContext from './IBaseAudioContext'; -export default interface IAnalyserNode - extends IAudioNode { +export default interface IAnalyserNode< + TContext extends IGenericBaseAudioContext, +> extends IGenericAudioNode { fftSize: number; readonly frequencyBinCount: number; minDecibels: number; diff --git a/packages/react-native-audio-api/src/types/interfaces/index.ts b/packages/react-native-audio-api/src/types/interfaces/index.ts index 0a5101ddd..b49eb9940 100644 --- a/packages/react-native-audio-api/src/types/interfaces/index.ts +++ b/packages/react-native-audio-api/src/types/interfaces/index.ts @@ -5,3 +5,4 @@ * * This means that underlying native and web implementations have differences. */ +export type { default as IAnalyserNode } from './IAnalyserNode'; From a86a3196f49c9ec0fcbad0c0e17edd8bcdc4a953 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20S=C4=99k?= Date: Thu, 2 Oct 2025 15:09:01 +0200 Subject: [PATCH 10/18] feat: ASSN int,classes --- .../core/sources/AudioScheduledSourceNode.ts | 39 ++++----- .../sources/AudioScheduledSourceNode.web.ts | 80 +++++-------------- .../sources/BaseAudioScheduledSourceNode.ts | 63 +++++++++++++++ .../interfaces/IAudioScheduledSourceNode.ts | 13 +++ .../src/types/interfaces/index.ts | 4 + .../internal/IAudioScheduledSourceNode.ts | 11 --- 6 files changed, 121 insertions(+), 89 deletions(-) create mode 100644 packages/react-native-audio-api/src/core/sources/BaseAudioScheduledSourceNode.ts create mode 100644 packages/react-native-audio-api/src/types/interfaces/IAudioScheduledSourceNode.ts delete mode 100644 packages/react-native-audio-api/src/types/internal/IAudioScheduledSourceNode.ts diff --git a/packages/react-native-audio-api/src/core/sources/AudioScheduledSourceNode.ts b/packages/react-native-audio-api/src/core/sources/AudioScheduledSourceNode.ts index 60a1bcdcb..45e3cb60b 100644 --- a/packages/react-native-audio-api/src/core/sources/AudioScheduledSourceNode.ts +++ b/packages/react-native-audio-api/src/core/sources/AudioScheduledSourceNode.ts @@ -1,31 +1,34 @@ import { AudioEventEmitter, AudioEventSubscription } from '../../events'; -import type { - IAudioScheduledSourceNode, - IBaseAudioContext, -} from '../../types/internal'; -import type { OnEndedEventCallback } from './AudioScheduledSourceNode.web'; -import AudioScheduledSourceNode from './AudioScheduledSourceNode.web'; - -interface INativeAudioScheduledSourceNode - extends IAudioScheduledSourceNode { +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 {} + +interface MobileAudioScheduledSourceNode + extends IAbstractNativeAudioScheduledSourceNode { onEnded: string; // subscriptionId or '0' for none } export default class AudioScheduledSourceNodeNative< - TContext extends IBaseAudioContext, - NContext extends IBaseAudioContext, - TNode extends - INativeAudioScheduledSourceNode = INativeAudioScheduledSourceNode, -> extends AudioScheduledSourceNode { + TContext extends IGenericBaseAudioContext, +> extends BaseAudioScheduledSourceNode< + TContext, + NativeAudioContext, + MobileAudioScheduledSourceNode +> { protected readonly audioEventEmitter = new AudioEventEmitter( global.AudioEventEmitter ); private onEndedSubscription?: AudioEventSubscription; - private onEndedCallbackNative: OnEndedEventCallback | null = null; + private onEndedCallback: OnEndedEventCallback | null = null; public get onEnded(): OnEndedEventCallback | undefined { - return this.onEndedCallbackNative ?? undefined; + return this.onEndedCallback ?? undefined; } public set onEnded(callback: OnEndedEventCallback | null) { @@ -33,11 +36,11 @@ export default class AudioScheduledSourceNodeNative< this.node.onEnded = '0'; this.onEndedSubscription?.remove(); this.onEndedSubscription = undefined; - this.onEndedCallbackNative = null; + this.onEndedCallback = null; return; } - this.onEndedCallbackNative = callback; + this.onEndedCallback = callback; this.onEndedSubscription = this.audioEventEmitter.addAudioEventListener( 'ended', callback 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 index 6482d82f9..08d095227 100644 --- a/packages/react-native-audio-api/src/core/sources/AudioScheduledSourceNode.web.ts +++ b/packages/react-native-audio-api/src/core/sources/AudioScheduledSourceNode.web.ts @@ -1,73 +1,33 @@ -import { InvalidStateError, RangeError } from '../../errors'; -import type { OnEndedEventType } from '../../events/types'; -import type { - IAudioScheduledSourceNode, - IBaseAudioContext, -} from '../../types/internal'; -import AudioNode from '../AudioNode'; +import type { IGenericBaseAudioContext } from '../../types/generics'; +import { OnEndedEventCallback } from '../../types/interfaces'; +import BaseAudioScheduledSourceNode from './BaseAudioScheduledSourceNode'; -export type OnEndedEventCallback = (event: OnEndedEventType) => void; +type NativeAudioContext = globalThis.BaseAudioContext; +type NativeAudioScheduledSourceNode = globalThis.AudioScheduledSourceNode; export default class AudioScheduledSourceNode< - TContext extends IBaseAudioContext, - NContext extends IBaseAudioContext, - TNode extends - IAudioScheduledSourceNode = IAudioScheduledSourceNode, - > - extends AudioNode - implements IAudioScheduledSourceNode -{ - protected hasBeenStarted: boolean = false; - private onEndedCallback: OnEndedEventCallback | null = null; - - 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); - } + TContext extends IGenericBaseAudioContext, +> extends BaseAudioScheduledSourceNode< + TContext, + NativeAudioContext, + NativeAudioScheduledSourceNode +> { + private onEndedCallback: OnEndedEventCallback | undefined = undefined; public get onEnded(): OnEndedEventCallback | undefined { - return this.onEndedCallback ?? undefined; + return this.onEndedCallback; } - public set onEnded(callback: OnEndedEventCallback | null) { - if (!callback) { - this.onEndedCallback = null; - (this.node as unknown as globalThis.AudioScheduledSourceNode).onended = - null; + public set onEnded(callback: OnEndedEventCallback | undefined) { + this.onEndedCallback = callback; + if (!callback) { + this.node.onended = null; return; } - this.onEndedCallback = callback; - (this.node as unknown as globalThis.AudioScheduledSourceNode).onended = - () => { - this.onEndedCallback?.({ bufferId: undefined }); - }; + this.node.onended = () => { + this.onEndedCallback?.({ bufferId: undefined }); + }; } } 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/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/index.ts b/packages/react-native-audio-api/src/types/interfaces/index.ts index b49eb9940..3b46db9ad 100644 --- a/packages/react-native-audio-api/src/types/interfaces/index.ts +++ b/packages/react-native-audio-api/src/types/interfaces/index.ts @@ -6,3 +6,7 @@ * This means that underlying native and web implementations have differences. */ export type { default as IAnalyserNode } from './IAnalyserNode'; +export type { + default as IAudioScheduledSourceNode, + OnEndedEventCallback, +} from './IAudioScheduledSourceNode'; diff --git a/packages/react-native-audio-api/src/types/internal/IAudioScheduledSourceNode.ts b/packages/react-native-audio-api/src/types/internal/IAudioScheduledSourceNode.ts deleted file mode 100644 index fbd25e05f..000000000 --- a/packages/react-native-audio-api/src/types/internal/IAudioScheduledSourceNode.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type IAudioNode from './IAudioNode'; -import type IBaseAudioContext from './IBaseAudioContext'; - -export default interface IAudioScheduledSourceNode< - TContext extends IBaseAudioContext, -> extends IAudioNode { - start(when?: number): void; - stop(when?: number): void; - - // TODO: what to do about onEnded=string(native) onEnded=function(TS) and onended=function(web) -} From 0eebef2d57bb327c13d2147ad0dab2973645919a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20S=C4=99k?= Date: Thu, 2 Oct 2025 15:41:58 +0200 Subject: [PATCH 11/18] feat: ASSN fixes, generic oscillator hack --- .../core/sources/AudioScheduledSourceNode.ts | 7 ++--- .../sources/AudioScheduledSourceNode.web.ts | 7 ++--- .../src/core/sources/OscillatorNode.ts | 26 ++++++++++------- .../generics/IGenericBiquadFilterNode.ts | 8 ++--- .../src/types/generics/IGenericGainNode.ts | 2 +- .../types/generics/IGenericOscillatorNode.ts | 29 ++++++++++++------- .../generics/IGenericStereoPannerNode.ts | 2 +- .../src/types/generics/index.ts | 1 + .../src/types/internal/IOscillatorNode.ts | 15 ---------- 9 files changed, 45 insertions(+), 52 deletions(-) delete mode 100644 packages/react-native-audio-api/src/types/internal/IOscillatorNode.ts diff --git a/packages/react-native-audio-api/src/core/sources/AudioScheduledSourceNode.ts b/packages/react-native-audio-api/src/core/sources/AudioScheduledSourceNode.ts index 45e3cb60b..833300820 100644 --- a/packages/react-native-audio-api/src/core/sources/AudioScheduledSourceNode.ts +++ b/packages/react-native-audio-api/src/core/sources/AudioScheduledSourceNode.ts @@ -15,11 +15,8 @@ interface MobileAudioScheduledSourceNode export default class AudioScheduledSourceNodeNative< TContext extends IGenericBaseAudioContext, -> extends BaseAudioScheduledSourceNode< - TContext, - NativeAudioContext, - MobileAudioScheduledSourceNode -> { + NNode extends MobileAudioScheduledSourceNode = MobileAudioScheduledSourceNode, +> extends BaseAudioScheduledSourceNode { protected readonly audioEventEmitter = new AudioEventEmitter( global.AudioEventEmitter ); 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 index 08d095227..055c61d84 100644 --- a/packages/react-native-audio-api/src/core/sources/AudioScheduledSourceNode.web.ts +++ b/packages/react-native-audio-api/src/core/sources/AudioScheduledSourceNode.web.ts @@ -7,11 +7,8 @@ type NativeAudioScheduledSourceNode = globalThis.AudioScheduledSourceNode; export default class AudioScheduledSourceNode< TContext extends IGenericBaseAudioContext, -> extends BaseAudioScheduledSourceNode< - TContext, - NativeAudioContext, - NativeAudioScheduledSourceNode -> { + NNode extends NativeAudioScheduledSourceNode = NativeAudioScheduledSourceNode, +> extends BaseAudioScheduledSourceNode { private onEndedCallback: OnEndedEventCallback | undefined = undefined; public get onEnded(): OnEndedEventCallback | undefined { diff --git a/packages/react-native-audio-api/src/core/sources/OscillatorNode.ts b/packages/react-native-audio-api/src/core/sources/OscillatorNode.ts index 9a4e2b603..af79b12b9 100644 --- a/packages/react-native-audio-api/src/core/sources/OscillatorNode.ts +++ b/packages/react-native-audio-api/src/core/sources/OscillatorNode.ts @@ -1,27 +1,31 @@ import { InvalidStateError } from '../../errors'; import type { OscillatorType } from '../../types'; -import type { IBaseAudioContext, IOscillatorNode } from '../../types/internal'; +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 IBaseAudioContext, - NContext extends IBaseAudioContext, + TContext extends IGenericBaseAudioContext, + NContext extends IGenericBaseAudioContext, > - extends AudioScheduledSourceNode< - TContext, - NContext, - IOscillatorNode - > - implements IOscillatorNode + // 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: IOscillatorNode) { + constructor(context: TContext, node: IGenericOscillatorNode) { super(context, node); + this.node = node; this.frequency = new AudioParam( node.frequency, @@ -45,7 +49,7 @@ export default class OscillatorNode< this.node.type = value; } - public setPeriodicWave(wave: PeriodicWave): void { + public setPeriodicWave(wave: PeriodicWave): void { this.node.setPeriodicWave(wave.periodicWave); } } diff --git a/packages/react-native-audio-api/src/types/generics/IGenericBiquadFilterNode.ts b/packages/react-native-audio-api/src/types/generics/IGenericBiquadFilterNode.ts index 595f8698c..1d21400aa 100644 --- a/packages/react-native-audio-api/src/types/generics/IGenericBiquadFilterNode.ts +++ b/packages/react-native-audio-api/src/types/generics/IGenericBiquadFilterNode.ts @@ -6,10 +6,10 @@ 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; + readonly frequency: IGenericAudioParam; + readonly detune: IGenericAudioParam; + readonly Q: IGenericAudioParam; + readonly gain: IGenericAudioParam; type: BiquadFilterType; getFrequencyResponse( diff --git a/packages/react-native-audio-api/src/types/generics/IGenericGainNode.ts b/packages/react-native-audio-api/src/types/generics/IGenericGainNode.ts index 6666c3d72..cccf138f6 100644 --- a/packages/react-native-audio-api/src/types/generics/IGenericGainNode.ts +++ b/packages/react-native-audio-api/src/types/generics/IGenericGainNode.ts @@ -5,5 +5,5 @@ import type IGenericBaseAudioContext from './IGenericBaseAudioContext'; export default interface IGenericGainNode< TContext extends IGenericBaseAudioContext, > extends IGenericAudioNode { - readonly gain: IGenericAudioParam; + 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 index a044addd0..fda91f114 100644 --- a/packages/react-native-audio-api/src/types/generics/IGenericOscillatorNode.ts +++ b/packages/react-native-audio-api/src/types/generics/IGenericOscillatorNode.ts @@ -1,15 +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'; -import type IAudioParam from './IAudioParam'; -import type IAudioScheduledSourceNode from './IAudioScheduledSourceNode'; -import type IBaseAudioContext from './IBaseAudioContext'; -import type IPeriodicWave from './IPeriodicWave'; - -export default interface IOscillatorNode - extends IAudioScheduledSourceNode { - readonly frequency: IAudioParam; - readonly detune: IAudioParam; +/** + * 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: IPeriodicWave): void; + setPeriodicWave(periodicWave: IGenericPeriodicWave): void; } diff --git a/packages/react-native-audio-api/src/types/generics/IGenericStereoPannerNode.ts b/packages/react-native-audio-api/src/types/generics/IGenericStereoPannerNode.ts index 7e884e760..75d58caa8 100644 --- a/packages/react-native-audio-api/src/types/generics/IGenericStereoPannerNode.ts +++ b/packages/react-native-audio-api/src/types/generics/IGenericStereoPannerNode.ts @@ -5,5 +5,5 @@ import type IGenericBaseAudioContext from './IGenericBaseAudioContext'; export default interface IGenericStereoPannerNode< TContext extends IGenericBaseAudioContext, > extends IGenericAudioNode { - readonly pan: IGenericAudioParam; + 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 index 2099cdc92..c3a80255a 100644 --- a/packages/react-native-audio-api/src/types/generics/index.ts +++ b/packages/react-native-audio-api/src/types/generics/index.ts @@ -19,5 +19,6 @@ export type { default as IGenericAudioParam } from './IGenericAudioParam'; 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 IGenericStereoPannerNode } from './IGenericStereoPannerNode'; diff --git a/packages/react-native-audio-api/src/types/internal/IOscillatorNode.ts b/packages/react-native-audio-api/src/types/internal/IOscillatorNode.ts deleted file mode 100644 index a044addd0..000000000 --- a/packages/react-native-audio-api/src/types/internal/IOscillatorNode.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { OscillatorType } from '../../types/properties'; - -import type IAudioParam from './IAudioParam'; -import type IAudioScheduledSourceNode from './IAudioScheduledSourceNode'; -import type IBaseAudioContext from './IBaseAudioContext'; -import type IPeriodicWave from './IPeriodicWave'; - -export default interface IOscillatorNode - extends IAudioScheduledSourceNode { - readonly frequency: IAudioParam; - readonly detune: IAudioParam; - type: OscillatorType; - - setPeriodicWave(periodicWave: IPeriodicWave): void; -} From ce68ff3d711297e5f5783bd821bf9f750cb10ff2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20S=C4=99k?= Date: Fri, 3 Oct 2025 16:09:25 +0200 Subject: [PATCH 12/18] feat: changes --- packages/react-native-audio-api/.eslintrc.js | 6 + .../react-native-audio-api/prev_src/api.ts | 74 +++--- .../core/AudioBufferBaseSourceNode.ts | 54 ----- .../prev_src/core/AudioBufferSourceNode.ts | 93 -------- .../prev_src/interfaces.ts | 11 - .../web-core/AudioBufferSourceNode.tsx | 4 +- .../AudioBufferBaseSourceNode.native.ts | 85 +++++++ .../sources/AudioBufferSourceNode.native.ts | 113 +++++++++ .../src/core/sources/AudioBufferSourceNode.ts | 0 .../core/sources/AudioBufferSourceNode.web.ts | 214 ++++++++++++++++++ ....ts => AudioScheduledSourceNode.native.ts} | 2 +- .../interfaces/IAudioBufferBaseSourceNode.ts | 17 ++ .../IAudioBufferSourceNode.ts | 11 +- .../src/types/interfaces/index.ts | 5 + .../internal/IAudioBufferBaseSourceNode.ts | 10 - .../src/utils/ActionQueue.ts | 49 ++++ .../src/utils/availabilityWarn.ts | 16 ++ .../src/utils/constants.ts | 1 + .../react-native-audio-api/src/utils/index.ts | 24 +- .../src/utils/makeDocLink.ts | 5 + packages/react-native-audio-api/tsconfig.json | 3 +- 21 files changed, 563 insertions(+), 234 deletions(-) delete mode 100644 packages/react-native-audio-api/prev_src/core/AudioBufferBaseSourceNode.ts delete mode 100644 packages/react-native-audio-api/prev_src/core/AudioBufferSourceNode.ts create mode 100644 packages/react-native-audio-api/src/core/sources/AudioBufferBaseSourceNode.native.ts create mode 100644 packages/react-native-audio-api/src/core/sources/AudioBufferSourceNode.native.ts delete mode 100644 packages/react-native-audio-api/src/core/sources/AudioBufferSourceNode.ts rename packages/react-native-audio-api/src/core/sources/{AudioScheduledSourceNode.ts => AudioScheduledSourceNode.native.ts} (97%) create mode 100644 packages/react-native-audio-api/src/types/interfaces/IAudioBufferBaseSourceNode.ts rename packages/react-native-audio-api/src/types/{internal => interfaces}/IAudioBufferSourceNode.ts (51%) delete mode 100644 packages/react-native-audio-api/src/types/internal/IAudioBufferBaseSourceNode.ts create mode 100644 packages/react-native-audio-api/src/utils/ActionQueue.ts create mode 100644 packages/react-native-audio-api/src/utils/availabilityWarn.ts create mode 100644 packages/react-native-audio-api/src/utils/constants.ts create mode 100644 packages/react-native-audio-api/src/utils/makeDocLink.ts diff --git a/packages/react-native-audio-api/.eslintrc.js b/packages/react-native-audio-api/.eslintrc.js index 4b0cc2ca4..471aaad94 100644 --- a/packages/react-native-audio-api/.eslintrc.js +++ b/packages/react-native-audio-api/.eslintrc.js @@ -7,4 +7,10 @@ module.exports = { }, ], ignorePatterns: ['lib', 'src/web-core/custom/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/prev_src/api.ts b/packages/react-native-audio-api/prev_src/api.ts index 656b799cd..e328758d8 100644 --- a/packages/react-native-audio-api/prev_src/api.ts +++ b/packages/react-native-audio-api/prev_src/api.ts @@ -37,42 +37,42 @@ if ( 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 { 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 { +// BiquadFilterType, +// ChannelCountMode, +// ChannelInterpretation, +// ContextState, +// OscillatorType, +// PeriodicWaveConstraints, +// WindowType, +// } from './types'; -export { - IndexSizeError, - InvalidAccessError, - InvalidStateError, - NotSupportedError, - RangeError, -} from './errors'; +// export { +// IndexSizeError, +// InvalidAccessError, +// InvalidStateError, +// NotSupportedError, +// RangeError, +// } from './errors'; diff --git a/packages/react-native-audio-api/prev_src/core/AudioBufferBaseSourceNode.ts b/packages/react-native-audio-api/prev_src/core/AudioBufferBaseSourceNode.ts deleted file mode 100644 index 28a409a75..000000000 --- a/packages/react-native-audio-api/prev_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 positionChangedSubscription?: AudioEventSubscription; - private positionChangedCallback?: (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.positionChangedCallback; - } - - public set onPositionChanged( - callback: ((event: EventTypeWithValue) => void) | null - ) { - if (!callback) { - (this.node as IAudioBufferBaseSourceNode).onPositionChanged = '0'; - this.positionChangedSubscription?.remove(); - this.positionChangedSubscription = undefined; - this.positionChangedCallback = undefined; - - return; - } - - this.positionChangedCallback = callback; - this.positionChangedSubscription = - this.audioEventEmitter.addAudioEventListener('positionChanged', callback); - - (this.node as IAudioBufferBaseSourceNode).onPositionChanged = - this.positionChangedSubscription.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/prev_src/core/AudioBufferSourceNode.ts b/packages/react-native-audio-api/prev_src/core/AudioBufferSourceNode.ts deleted file mode 100644 index 50efe55c1..000000000 --- a/packages/react-native-audio-api/prev_src/core/AudioBufferSourceNode.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { InvalidStateError, RangeError } from '../errors'; -import { EventEmptyType } from '../events/types'; -import { IAudioBufferSourceNode } from '../interfaces'; -import AudioBuffer from './AudioBuffer'; -import AudioBufferBaseSourceNode from './AudioBufferBaseSourceNode'; - -export default class AudioBufferSourceNode extends AudioBufferBaseSourceNode { - // 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; - // } -} diff --git a/packages/react-native-audio-api/prev_src/interfaces.ts b/packages/react-native-audio-api/prev_src/interfaces.ts index d39b529ea..ee25965d5 100644 --- a/packages/react-native-audio-api/prev_src/interfaces.ts +++ b/packages/react-native-audio-api/prev_src/interfaces.ts @@ -57,21 +57,10 @@ export interface IOfflineAudioContext extends IBaseAudioContext { startRendering(): Promise; } -export interface IAudioBufferBaseSourceNode extends IAudioScheduledSourceNode { - // 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 IStreamerNode extends IAudioNode { initialize(streamPath: string): boolean; } -export interface IAudioBufferSourceNode extends IAudioBufferBaseSourceNode { - setBuffer: (audioBuffer: IAudioBuffer | null) => void; -} - export interface IAudioBufferQueueSourceNode extends IAudioBufferBaseSourceNode { dequeueBuffer: (bufferId: number) => void; diff --git a/packages/react-native-audio-api/prev_src/web-core/AudioBufferSourceNode.tsx b/packages/react-native-audio-api/prev_src/web-core/AudioBufferSourceNode.tsx index 4ca212030..c9cc741c1 100644 --- a/packages/react-native-audio-api/prev_src/web-core/AudioBufferSourceNode.tsx +++ b/packages/react-native-audio-api/prev_src/web-core/AudioBufferSourceNode.tsx @@ -1,9 +1,9 @@ import { InvalidStateError, RangeError } from '../errors'; -import AudioParam from './AudioParam'; import AudioBuffer from './AudioBuffer'; -import BaseAudioContext from './BaseAudioContext'; +import AudioParam from './AudioParam'; import AudioScheduledSourceNode from './AudioScheduledSourceNode'; +import BaseAudioContext from './BaseAudioContext'; import { clamp } from '../utils'; import { globalTag } from './custom/LoadCustomWasm'; 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/AudioBufferSourceNode.native.ts b/packages/react-native-audio-api/src/core/sources/AudioBufferSourceNode.native.ts new file mode 100644 index 000000000..6a6eb9973 --- /dev/null +++ b/packages/react-native-audio-api/src/core/sources/AudioBufferSourceNode.native.ts @@ -0,0 +1,113 @@ +import { InvalidStateError, RangeError } from '../../errors'; +// import { EventEmptyType } from '../../events'; +import type { + IGenericAudioBuffer, + IGenericBaseAudioContext, +} from '../../types/generics'; +import type { IAudioBufferSourceNode } 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; +} + +export default class AudioBufferSourceNode< + TContext extends IGenericBaseAudioContext, + > + extends AudioBufferBaseSourceNode + implements IAudioBufferSourceNode +{ + public get buffer(): AudioBuffer | null { + if (!this.node.buffer) { + return null; + } + + return new AudioBuffer(this.node.buffer); + } + + public set buffer(buffer: IGenericAudioBuffer | null) { + this.node.setBuffer(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); + } + + // ????????????????????????? + // 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.ts b/packages/react-native-audio-api/src/core/sources/AudioBufferSourceNode.ts deleted file mode 100644 index e69de29bb..000000000 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 index e69de29bb..648111db3 100644 --- a/packages/react-native-audio-api/src/core/sources/AudioBufferSourceNode.web.ts +++ b/packages/react-native-audio-api/src/core/sources/AudioBufferSourceNode.web.ts @@ -0,0 +1,214 @@ +import { InvalidStateError, RangeError } from '../../errors'; + +import type { + IGenericAudioBuffer, + IGenericBaseAudioContext, +} from '../../types/generics'; +import type { IAudioBufferSourceNode } from '../../types/interfaces'; +import { ActionQueue } from '../../utils'; +import AudioBuffer from '../AudioBuffer'; +import AudioParam from '../AudioParam'; + +type NativeAudioContext = globalThis.BaseAudioContext; +type NativeAudioBufferSourceNode = globalThis.AudioBufferSourceNode; + +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 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; + } +} + +type WebBufferSourceNode = NativeAudioBufferSourceNode | IStretcherNode; + +// Note: this class do not extend from AudioScheduledSourceNode or AudioNode +// because of the promise based access nature of the stretcher node +export default class AudioBufferSourceNode< + TContext extends IGenericBaseAudioContext, + NNode extends WebBufferSourceNode = NativeAudioBufferSourceNode, +> implements IAudioBufferSourceNode +{ + readonly playbackRate: AudioParam; + readonly detune: AudioParam; + + private mPitchCorrection: boolean; + + private mLoop: boolean = false; + private mLoopStart: number = 0; + private mLoopEnd: number = 0; + + private mBuffer: AudioBuffer | null = null; + + private actionQueue = new ActionQueue(); + + private hasBeenStarted = false; + + constructor( + context: TContext, + nodePromise: Promise, + pitchCorrection: boolean + ) { + this.mPitchCorrection = pitchCorrection; + + if (!pitchCorrection) { + this.playbackRate = new AudioParam(null, context); + this.detune = new AudioParam(null, context); + } + } +} diff --git a/packages/react-native-audio-api/src/core/sources/AudioScheduledSourceNode.ts b/packages/react-native-audio-api/src/core/sources/AudioScheduledSourceNode.native.ts similarity index 97% rename from packages/react-native-audio-api/src/core/sources/AudioScheduledSourceNode.ts rename to packages/react-native-audio-api/src/core/sources/AudioScheduledSourceNode.native.ts index 833300820..780a536cf 100644 --- a/packages/react-native-audio-api/src/core/sources/AudioScheduledSourceNode.ts +++ b/packages/react-native-audio-api/src/core/sources/AudioScheduledSourceNode.native.ts @@ -8,7 +8,7 @@ import BaseAudioScheduledSourceNode, { // TODO: fixme - temporary any to avoid work interface NativeAudioContext {} -interface MobileAudioScheduledSourceNode +export interface MobileAudioScheduledSourceNode extends IAbstractNativeAudioScheduledSourceNode { onEnded: string; // subscriptionId or '0' for none } 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/internal/IAudioBufferSourceNode.ts b/packages/react-native-audio-api/src/types/interfaces/IAudioBufferSourceNode.ts similarity index 51% rename from packages/react-native-audio-api/src/types/internal/IAudioBufferSourceNode.ts rename to packages/react-native-audio-api/src/types/interfaces/IAudioBufferSourceNode.ts index 2f53cacb8..b497277f5 100644 --- a/packages/react-native-audio-api/src/types/internal/IAudioBufferSourceNode.ts +++ b/packages/react-native-audio-api/src/types/interfaces/IAudioBufferSourceNode.ts @@ -1,15 +1,14 @@ -import type IAudioBuffer from './IAudioBuffer'; +import type { IGenericBaseAudioContext } from '../generics'; import type IAudioBufferBaseSourceNode from './IAudioBufferBaseSourceNode'; -import type IBaseAudioContext from './IBaseAudioContext'; export default interface IAudioBufferSourceNode< - TContext extends IBaseAudioContext, + TContext extends IGenericBaseAudioContext, > extends IAudioBufferBaseSourceNode { - buffer: IAudioBuffer | null; - loop: boolean; + buffer: AudioBuffer | null; loopSkip: boolean; + loop: boolean; loopStart: number; loopEnd: number; - start: (when?: number, offset?: number, duration?: number) => void; + start(when?: number, offset?: number, duration?: number): void; } diff --git a/packages/react-native-audio-api/src/types/interfaces/index.ts b/packages/react-native-audio-api/src/types/interfaces/index.ts index 3b46db9ad..3f020d812 100644 --- a/packages/react-native-audio-api/src/types/interfaces/index.ts +++ b/packages/react-native-audio-api/src/types/interfaces/index.ts @@ -6,6 +6,11 @@ * 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 IAudioBufferSourceNode } from './IAudioBufferSourceNode'; export type { default as IAudioScheduledSourceNode, OnEndedEventCallback, diff --git a/packages/react-native-audio-api/src/types/internal/IAudioBufferBaseSourceNode.ts b/packages/react-native-audio-api/src/types/internal/IAudioBufferBaseSourceNode.ts deleted file mode 100644 index 5e0949923..000000000 --- a/packages/react-native-audio-api/src/types/internal/IAudioBufferBaseSourceNode.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type IAudioParam from './IAudioParam'; -import type IAudioScheduledSourceNode from './IAudioScheduledSourceNode'; -import type IBaseAudioContext from './IBaseAudioContext'; - -export default interface IAudioBufferBaseSourceNode< - TContext extends IBaseAudioContext, -> extends IAudioScheduledSourceNode { - detune: IAudioParam; - playbackRate: IAudioParam; -} diff --git a/packages/react-native-audio-api/src/utils/ActionQueue.ts b/packages/react-native-audio-api/src/utils/ActionQueue.ts new file mode 100644 index 000000000..878cb69c0 --- /dev/null +++ b/packages/react-native-audio-api/src/utils/ActionQueue.ts @@ -0,0 +1,49 @@ +type AsyncAction = () => T | Promise; + +export default class ActionQueue { + private queue: AsyncAction[] = []; + private isProcessing = false; + + /** + * Add an async action to the queue. + * @param action - A function that returns a Promise. + * @returns A Promise that resolves or rejects with the action's result. + */ + public enqueue(action: AsyncAction): Promise { + return new Promise((resolve, reject) => { + const wrappedAction = async () => { + try { + const result = await action(); + resolve(result); + } catch (err) { + reject(err); + } + }; + + this.queue.push(wrappedAction); + this.processQueue(); + }); + } + + /** + * Internal method to process the queue. + */ + private async processQueue(): Promise { + if (this.isProcessing || this.queue.length === 0) { + return; + } + + this.isProcessing = true; + + const nextAction = this.queue.shift(); + + if (nextAction) { + try { + await nextAction(); + } finally { + this.isProcessing = false; + this.processQueue(); + } + } + } +} 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 c1bf6add8..b6d70b880 100644 --- a/packages/react-native-audio-api/src/utils/index.ts +++ b/packages/react-native-audio-api/src/utils/index.ts @@ -1,22 +1,8 @@ -export const constants = { - baseUrl: 'https://docs.swmansion.com/react-native-audio-api', -} as const; +import * as constants from './constants'; -export function makeDocLink(path: string) { - return `${constants.baseUrl}${path}`; -} +export { default as availabilityWarn } from './availabilityWarn'; +export { default as makeDocLink } from './makeDocLink'; -export function availabilityWarn( - feature: string, - platform: 'web' | 'native' = 'web', - docPath?: string -) { - const baseMsg = `The ${feature} is not available on ${platform} platform.`; +export { constants }; - if (!docPath) { - console.warn(baseMsg); - return; - } - - console.warn(`${baseMsg} See ${makeDocLink(docPath)} for more information.`); -} +export { default as ActionQueue } from './ActionQueue'; 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/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"] } From 2fc89fca94527fa1562bc09a550524802b2860a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20S=C4=99k?= Date: Mon, 6 Oct 2025 13:42:28 +0200 Subject: [PATCH 13/18] fix: split web ABSN into multiple smaller files --- .../core/sources/ABSNWeb/IStretcherNode.ts | 69 ++++++++ .../ABSNWeb/StretcherNodeAudioParam.ts | 92 ++++++++++ .../src/core/sources/ABSNWeb/index.ts | 0 .../core/sources/AudioBufferSourceNode.web.ts | 165 +----------------- 4 files changed, 162 insertions(+), 164 deletions(-) create mode 100644 packages/react-native-audio-api/src/core/sources/ABSNWeb/IStretcherNode.ts create mode 100644 packages/react-native-audio-api/src/core/sources/ABSNWeb/StretcherNodeAudioParam.ts create mode 100644 packages/react-native-audio-api/src/core/sources/ABSNWeb/index.ts 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/index.ts b/packages/react-native-audio-api/src/core/sources/ABSNWeb/index.ts new file mode 100644 index 000000000..e69de29bb 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 index 648111db3..ead39d0f6 100644 --- a/packages/react-native-audio-api/src/core/sources/AudioBufferSourceNode.web.ts +++ b/packages/react-native-audio-api/src/core/sources/AudioBufferSourceNode.web.ts @@ -12,170 +12,7 @@ import AudioParam from '../AudioParam'; type NativeAudioContext = globalThis.BaseAudioContext; type NativeAudioBufferSourceNode = globalThis.AudioBufferSourceNode; -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 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; - } -} - -type WebBufferSourceNode = NativeAudioBufferSourceNode | IStretcherNode; +type WebBufferSourceNode = NativeAudioBufferSourceNode; // Note: this class do not extend from AudioScheduledSourceNode or AudioNode // because of the promise based access nature of the stretcher node From 1b68204e8d61eab30820f7bba280df62af9d6073 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20S=C4=99k?= Date: Wed, 8 Oct 2025 18:32:05 +0200 Subject: [PATCH 14/18] feat: absn almost ready --- .../custom-node-generator/lib/generator.js | 55 --- packages/react-native-audio-api/.eslintrc.js | 26 +- .../common/cpp/audioapi/utils/AudioBus.cpp | 4 + .../web-core/AudioBufferSourceNode.tsx | 436 ------------------ .../web-core/custom/LoadCustomWasm.ts | 39 -- .../prev_src/web-core/custom/index.ts | 1 - .../core/sources/ABSNWeb/StretcherProvider.ts | 59 +++ .../src/core/sources/ABSNWeb/index.ts | 3 + .../core/sources/AudioBufferSourceNode.web.ts | 399 +++++++++++++++- .../src/external/constants.ts | 2 + .../src/external/customWasmLoader.native.ts | 16 + .../src/external/customWasmLoader.web.ts | 54 +++ .../src/external/index.ts | 2 + .../src/external/interface.ts | 4 + .../external}/signalsmithStretch/LICENSE.txt | 0 .../external}/signalsmithStretch/README.md | 0 .../signalsmithStretch/SignalsmithStretch.mjs | 0 .../src/types/generics/IGenericAudioNode.ts | 5 +- .../src/utils/ActionQueue.ts | 49 -- .../react-native-audio-api/src/utils/index.ts | 2 - 20 files changed, 550 insertions(+), 606 deletions(-) delete mode 100644 packages/custom-node-generator/lib/generator.js delete mode 100644 packages/react-native-audio-api/prev_src/web-core/AudioBufferSourceNode.tsx delete mode 100644 packages/react-native-audio-api/prev_src/web-core/custom/LoadCustomWasm.ts delete mode 100644 packages/react-native-audio-api/prev_src/web-core/custom/index.ts create mode 100644 packages/react-native-audio-api/src/core/sources/ABSNWeb/StretcherProvider.ts create mode 100644 packages/react-native-audio-api/src/external/constants.ts create mode 100644 packages/react-native-audio-api/src/external/customWasmLoader.native.ts create mode 100644 packages/react-native-audio-api/src/external/customWasmLoader.web.ts create mode 100644 packages/react-native-audio-api/src/external/index.ts create mode 100644 packages/react-native-audio-api/src/external/interface.ts rename packages/react-native-audio-api/{prev_src/web-core/custom => src/external}/signalsmithStretch/LICENSE.txt (100%) rename packages/react-native-audio-api/{prev_src/web-core/custom => src/external}/signalsmithStretch/README.md (100%) rename packages/react-native-audio-api/{prev_src/web-core/custom => src/external}/signalsmithStretch/SignalsmithStretch.mjs (100%) delete mode 100644 packages/react-native-audio-api/src/utils/ActionQueue.ts 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 471aaad94..01cdc221c 100644 --- a/packages/react-native-audio-api/.eslintrc.js +++ b/packages/react-native-audio-api/.eslintrc.js @@ -6,11 +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"] - } - } + 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/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/prev_src/web-core/AudioBufferSourceNode.tsx b/packages/react-native-audio-api/prev_src/web-core/AudioBufferSourceNode.tsx deleted file mode 100644 index c9cc741c1..000000000 --- a/packages/react-native-audio-api/prev_src/web-core/AudioBufferSourceNode.tsx +++ /dev/null @@ -1,436 +0,0 @@ -import { InvalidStateError, RangeError } from '../errors'; - -import AudioBuffer from './AudioBuffer'; -import AudioParam from './AudioParam'; -import AudioScheduledSourceNode from './AudioScheduledSourceNode'; -import BaseAudioContext from './BaseAudioContext'; - -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/prev_src/web-core/custom/LoadCustomWasm.ts b/packages/react-native-audio-api/prev_src/web-core/custom/LoadCustomWasm.ts deleted file mode 100644 index d0a608c61..000000000 --- a/packages/react-native-audio-api/prev_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/prev_src/web-core/custom/index.ts b/packages/react-native-audio-api/prev_src/web-core/custom/index.ts deleted file mode 100644 index d2a7623ca..000000000 --- a/packages/react-native-audio-api/prev_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/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 index e69de29bb..43abfadbf 100644 --- a/packages/react-native-audio-api/src/core/sources/ABSNWeb/index.ts +++ 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/AudioBufferSourceNode.web.ts b/packages/react-native-audio-api/src/core/sources/AudioBufferSourceNode.web.ts index ead39d0f6..50c0e8014 100644 --- a/packages/react-native-audio-api/src/core/sources/AudioBufferSourceNode.web.ts +++ b/packages/react-native-audio-api/src/core/sources/AudioBufferSourceNode.web.ts @@ -1,51 +1,416 @@ import { InvalidStateError, RangeError } from '../../errors'; +import type { ChannelCountMode, ChannelInterpretation } from '../../types'; +import type { IGenericBaseAudioContext } from '../../types/generics'; import type { - IGenericAudioBuffer, - IGenericBaseAudioContext, -} from '../../types/generics'; -import type { IAudioBufferSourceNode } from '../../types/interfaces'; -import { ActionQueue } from '../../utils'; + IAudioBufferSourceNode, + 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; +} -type WebBufferSourceNode = NativeAudioBufferSourceNode; +function isNativeNode( + node: WebBufferSourceNode, + pitchCorrection: boolean +): node is NativeAudioBufferSourceNode { + return ( + !pitchCorrection && + (node as NativeAudioBufferSourceNode).buffer !== undefined + ); +} -// Note: this class do not extend from AudioScheduledSourceNode or AudioNode -// because of the promise based access nature of the stretcher node export default class AudioBufferSourceNode< TContext extends IGenericBaseAudioContext, - NNode extends WebBufferSourceNode = NativeAudioBufferSourceNode, > 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 actionQueue = new ActionQueue(); - - private hasBeenStarted = false; + private mHasBeenStarted = false; + private mCallback: OnEndedEventCallback | null = null; constructor( context: TContext, - nodePromise: Promise, + node: WebBufferSourceNode, pitchCorrection: boolean ) { + this.mNode = node; + this.context = context; this.mPitchCorrection = pitchCorrection; - if (!pitchCorrection) { - this.playbackRate = new AudioParam(null, context); - this.detune = new AudioParam(null, context); + 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 }); + }; + } + + 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/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/prev_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/prev_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/prev_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/prev_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/prev_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/prev_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/types/generics/IGenericAudioNode.ts b/packages/react-native-audio-api/src/types/generics/IGenericAudioNode.ts index f5a8af83f..c0eb87cc0 100644 --- a/packages/react-native-audio-api/src/types/generics/IGenericAudioNode.ts +++ b/packages/react-native-audio-api/src/types/generics/IGenericAudioNode.ts @@ -1,3 +1,4 @@ +import type { ChannelCountMode, ChannelInterpretation } from '../../types'; import type IGenericAudioParam from './IGenericAudioParam'; import type IGenericBaseAudioContext from './IGenericBaseAudioContext'; @@ -14,7 +15,7 @@ export default interface IGenericAudioNode< connect>( destination: D ): D; - connect(destination: IGenericAudioParam): void; + connect(destination: IGenericAudioParam): void; disconnect< C extends IGenericBaseAudioContext, @@ -22,5 +23,5 @@ export default interface IGenericAudioNode< >( destination?: D ): void; - disconnect(destination?: IGenericAudioParam): void; + disconnect(destination?: IGenericAudioParam): void; } diff --git a/packages/react-native-audio-api/src/utils/ActionQueue.ts b/packages/react-native-audio-api/src/utils/ActionQueue.ts deleted file mode 100644 index 878cb69c0..000000000 --- a/packages/react-native-audio-api/src/utils/ActionQueue.ts +++ /dev/null @@ -1,49 +0,0 @@ -type AsyncAction = () => T | Promise; - -export default class ActionQueue { - private queue: AsyncAction[] = []; - private isProcessing = false; - - /** - * Add an async action to the queue. - * @param action - A function that returns a Promise. - * @returns A Promise that resolves or rejects with the action's result. - */ - public enqueue(action: AsyncAction): Promise { - return new Promise((resolve, reject) => { - const wrappedAction = async () => { - try { - const result = await action(); - resolve(result); - } catch (err) { - reject(err); - } - }; - - this.queue.push(wrappedAction); - this.processQueue(); - }); - } - - /** - * Internal method to process the queue. - */ - private async processQueue(): Promise { - if (this.isProcessing || this.queue.length === 0) { - return; - } - - this.isProcessing = true; - - const nextAction = this.queue.shift(); - - if (nextAction) { - try { - await nextAction(); - } finally { - this.isProcessing = false; - this.processQueue(); - } - } - } -} diff --git a/packages/react-native-audio-api/src/utils/index.ts b/packages/react-native-audio-api/src/utils/index.ts index b6d70b880..9c8799c90 100644 --- a/packages/react-native-audio-api/src/utils/index.ts +++ b/packages/react-native-audio-api/src/utils/index.ts @@ -4,5 +4,3 @@ export { default as availabilityWarn } from './availabilityWarn'; export { default as makeDocLink } from './makeDocLink'; export { constants }; - -export { default as ActionQueue } from './ActionQueue'; From 49d5a8d48bfc5fd6fbee4dbd3cff7e87e2a898ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20S=C4=99k?= Date: Thu, 9 Oct 2025 18:58:05 +0200 Subject: [PATCH 15/18] feat: abqsn --- .../core/AudioBufferQueueSourceNode.ts | 58 ------- .../prev_src/interfaces.ts | 10 -- .../AudioBufferQueueSourceNode.native.ts | 72 ++++++++ .../sources/AudioBufferQueueSourcenode.web.ts | 159 ++++++++++++++++++ .../interfaces/IAudioBufferQueueSourceNode.ts | 13 ++ .../src/types/interfaces/index.ts | 1 + 6 files changed, 245 insertions(+), 68 deletions(-) delete mode 100644 packages/react-native-audio-api/prev_src/core/AudioBufferQueueSourceNode.ts create mode 100644 packages/react-native-audio-api/src/core/sources/AudioBufferQueueSourceNode.native.ts create mode 100644 packages/react-native-audio-api/src/core/sources/AudioBufferQueueSourcenode.web.ts create mode 100644 packages/react-native-audio-api/src/types/interfaces/IAudioBufferQueueSourceNode.ts diff --git a/packages/react-native-audio-api/prev_src/core/AudioBufferQueueSourceNode.ts b/packages/react-native-audio-api/prev_src/core/AudioBufferQueueSourceNode.ts deleted file mode 100644 index bfc923d2a..000000000 --- a/packages/react-native-audio-api/prev_src/core/AudioBufferQueueSourceNode.ts +++ /dev/null @@ -1,58 +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, offset?: number): void { - if (when < 0) { - throw new RangeError( - `when must be a finite non-negative number: ${when}` - ); - } - - if (offset) { - if (offset < 0) { - throw new RangeError( - `offset must be a finite non-negative number: ${offset}` - ); - } - } - - (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/prev_src/interfaces.ts b/packages/react-native-audio-api/prev_src/interfaces.ts index ee25965d5..5c84e9e3d 100644 --- a/packages/react-native-audio-api/prev_src/interfaces.ts +++ b/packages/react-native-audio-api/prev_src/interfaces.ts @@ -61,16 +61,6 @@ export interface IStreamerNode extends IAudioNode { initialize(streamPath: string): boolean; } -export interface IAudioBufferQueueSourceNode - extends IAudioBufferBaseSourceNode { - dequeueBuffer: (bufferId: number) => void; - clearBuffers: () => void; - - // returns bufferId - enqueueBuffer: (audioBuffer: IAudioBuffer) => string; - pause: () => void; -} - export interface IRecorderAdapterNode extends IAudioNode {} export interface IWorkletNode extends IAudioNode {} 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..d7a80d4b2 --- /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, _offset?: number): 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..b5f56bcf0 --- /dev/null +++ b/packages/react-native-audio-api/src/core/sources/AudioBufferQueueSourcenode.web.ts @@ -0,0 +1,159 @@ +import type { ChannelCountMode, ChannelInterpretation } from '../../types'; +import type { IGenericBaseAudioContext } from '../../types/generics'; +import type { + IAudioBufferQueueSourceNode, + OnEndedEventCallback, + OnPositionChangedEventCallback, +} from '../../types/interfaces'; +import { availabilityWarn } from '../../utils'; +import AudioBuffer from '../AudioBuffer'; +import AudioNode from '../AudioNode'; +import AudioParam from '../AudioParam'; + +type NativeContext = globalThis.BaseAudioContext; + +export default class AudioBufferQueueSourceNode< + TContext extends IGenericBaseAudioContext, +> implements IAudioBufferQueueSourceNode +{ + readonly context: TContext; + readonly numberOfInputs = 0; + readonly numberOfOutputs = 1; + readonly channelCount = 2; + readonly channelCountMode: ChannelCountMode = 'max'; + readonly channelInterpretation: ChannelInterpretation = 'speakers'; + + readonly playbackRate: AudioParam; + readonly detune: AudioParam; + + constructor(context: TContext) { + this.context = context; + + // Dummy AudioParams + this.playbackRate = new AudioParam( + { + defaultValue: 1, + minValue: -3.4028234663852886e38, + maxValue: 3.4028234663852886e38, + value: 1, + setValueAtTime: () => this.playbackRate, + linearRampToValueAtTime: () => this.playbackRate, + exponentialRampToValueAtTime: () => this.playbackRate, + setTargetAtTime: () => this.playbackRate, + setValueCurveAtTime: () => this.playbackRate, + cancelScheduledValues: () => this.playbackRate, + cancelAndHoldAtTime: () => this.playbackRate, + }, + context + ); + + this.detune = new AudioParam( + { + defaultValue: 0, + minValue: -3.4028234663852886e38, + maxValue: 3.4028234663852886e38, + value: 0, + setValueAtTime: () => this.detune, + linearRampToValueAtTime: () => this.detune, + exponentialRampToValueAtTime: () => this.detune, + setTargetAtTime: () => this.detune, + setValueCurveAtTime: () => this.detune, + cancelScheduledValues: () => this.detune, + cancelAndHoldAtTime: () => this.detune, + }, + context + ); + } + + public enqueueBuffer(_buffer: AudioBuffer): string { + availabilityWarn('AudioBufferQueueSourceNode.enqueueBuffer', 'web'); + return ''; + } + + public dequeueBuffer(_bufferId: string): void { + availabilityWarn('AudioBufferQueueSourceNode.dequeueBuffer', 'web'); + } + + public clearBuffers(): void { + availabilityWarn('AudioBufferQueueSourceNode.clearBuffers', 'web'); + } + + public start(_when: number = 0, _offset?: number): void { + availabilityWarn('AudioBufferQueueSourceNode.start', 'web'); + } + + public pause(): void { + availabilityWarn('AudioBufferQueueSourceNode.pause', 'web'); + } + + public stop(_when: number = 0): void { + availabilityWarn('AudioBufferQueueSourceNode.stop', 'web'); + } + + public connect>( + destination: ONode + ): ONode; + + public connect(destination: AudioParam): void; + + public connect>( + destination: ONode | AudioParam + ): ONode | void { + availabilityWarn('AudioBufferQueueSourceNode.connect', 'web'); + + if (destination instanceof AudioParam) { + return; + } + + return destination; + } + + public disconnect(): void; + + public disconnect>( + destination: ONode + ): void; + + public disconnect(destination: AudioParam): void; + + public disconnect( + _destination?: + | AudioNode + | AudioParam + ): void {} + + public get onEnded(): OnEndedEventCallback | undefined { + availabilityWarn('AudioBufferQueueSourceNode.onEnded', 'web'); + return undefined; + } + + public set onEnded(_callback: OnEndedEventCallback | undefined) { + availabilityWarn('AudioBufferQueueSourceNode.onEnded', 'web'); + } + + public get onPositionChangedInterval(): number { + availabilityWarn( + 'AudioBufferQueueSourceNode.onPositionChangedInterval', + 'web' + ); + return 0; + } + + public set onPositionChangedInterval(_value: number) { + availabilityWarn( + 'AudioBufferQueueSourceNode.onPositionChangedInterval', + 'web' + ); + } + + public get onPositionChanged(): OnPositionChangedEventCallback | undefined { + availabilityWarn('AudioBufferQueueSourceNode.onPositionChanged', 'web'); + return undefined; + } + + public set onPositionChanged( + _callback: OnPositionChangedEventCallback | null + ) { + availabilityWarn('AudioBufferQueueSourceNode.onPositionChanged', 'web'); + } +} 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/index.ts b/packages/react-native-audio-api/src/types/interfaces/index.ts index 3f020d812..3bcadd8f7 100644 --- a/packages/react-native-audio-api/src/types/interfaces/index.ts +++ b/packages/react-native-audio-api/src/types/interfaces/index.ts @@ -10,6 +10,7 @@ export type { default as IAudioBufferBaseSourceNode, OnPositionChangedEventCallback, } from './IAudioBufferBaseSourceNode'; +export type { default as IAudioBufferQueueSourceNode } from './IAudioBufferQueueSourceNode'; export type { default as IAudioBufferSourceNode } from './IAudioBufferSourceNode'; export type { default as IAudioScheduledSourceNode, From 6e5ddb36c00aaf7304854010a6850baf9459ddc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20S=C4=99k?= Date: Thu, 9 Oct 2025 19:17:07 +0200 Subject: [PATCH 16/18] fix: simplify? missing web interfaces --- .../sources/AudioBufferQueueSourcenode.web.ts | 160 +----------------- 1 file changed, 4 insertions(+), 156 deletions(-) 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 index b5f56bcf0..c84ad81a4 100644 --- a/packages/react-native-audio-api/src/core/sources/AudioBufferQueueSourcenode.web.ts +++ b/packages/react-native-audio-api/src/core/sources/AudioBufferQueueSourcenode.web.ts @@ -1,159 +1,7 @@ -import type { ChannelCountMode, ChannelInterpretation } from '../../types'; import type { IGenericBaseAudioContext } from '../../types/generics'; -import type { - IAudioBufferQueueSourceNode, - OnEndedEventCallback, - OnPositionChangedEventCallback, -} from '../../types/interfaces'; -import { availabilityWarn } from '../../utils'; -import AudioBuffer from '../AudioBuffer'; -import AudioNode from '../AudioNode'; -import AudioParam from '../AudioParam'; +import type { IAudioBufferQueueSourceNode } from '../../types/interfaces'; -type NativeContext = globalThis.BaseAudioContext; +class AudioBufferQueueSourceNode {} -export default class AudioBufferQueueSourceNode< - TContext extends IGenericBaseAudioContext, -> implements IAudioBufferQueueSourceNode -{ - readonly context: TContext; - readonly numberOfInputs = 0; - readonly numberOfOutputs = 1; - readonly channelCount = 2; - readonly channelCountMode: ChannelCountMode = 'max'; - readonly channelInterpretation: ChannelInterpretation = 'speakers'; - - readonly playbackRate: AudioParam; - readonly detune: AudioParam; - - constructor(context: TContext) { - this.context = context; - - // Dummy AudioParams - this.playbackRate = new AudioParam( - { - defaultValue: 1, - minValue: -3.4028234663852886e38, - maxValue: 3.4028234663852886e38, - value: 1, - setValueAtTime: () => this.playbackRate, - linearRampToValueAtTime: () => this.playbackRate, - exponentialRampToValueAtTime: () => this.playbackRate, - setTargetAtTime: () => this.playbackRate, - setValueCurveAtTime: () => this.playbackRate, - cancelScheduledValues: () => this.playbackRate, - cancelAndHoldAtTime: () => this.playbackRate, - }, - context - ); - - this.detune = new AudioParam( - { - defaultValue: 0, - minValue: -3.4028234663852886e38, - maxValue: 3.4028234663852886e38, - value: 0, - setValueAtTime: () => this.detune, - linearRampToValueAtTime: () => this.detune, - exponentialRampToValueAtTime: () => this.detune, - setTargetAtTime: () => this.detune, - setValueCurveAtTime: () => this.detune, - cancelScheduledValues: () => this.detune, - cancelAndHoldAtTime: () => this.detune, - }, - context - ); - } - - public enqueueBuffer(_buffer: AudioBuffer): string { - availabilityWarn('AudioBufferQueueSourceNode.enqueueBuffer', 'web'); - return ''; - } - - public dequeueBuffer(_bufferId: string): void { - availabilityWarn('AudioBufferQueueSourceNode.dequeueBuffer', 'web'); - } - - public clearBuffers(): void { - availabilityWarn('AudioBufferQueueSourceNode.clearBuffers', 'web'); - } - - public start(_when: number = 0, _offset?: number): void { - availabilityWarn('AudioBufferQueueSourceNode.start', 'web'); - } - - public pause(): void { - availabilityWarn('AudioBufferQueueSourceNode.pause', 'web'); - } - - public stop(_when: number = 0): void { - availabilityWarn('AudioBufferQueueSourceNode.stop', 'web'); - } - - public connect>( - destination: ONode - ): ONode; - - public connect(destination: AudioParam): void; - - public connect>( - destination: ONode | AudioParam - ): ONode | void { - availabilityWarn('AudioBufferQueueSourceNode.connect', 'web'); - - if (destination instanceof AudioParam) { - return; - } - - return destination; - } - - public disconnect(): void; - - public disconnect>( - destination: ONode - ): void; - - public disconnect(destination: AudioParam): void; - - public disconnect( - _destination?: - | AudioNode - | AudioParam - ): void {} - - public get onEnded(): OnEndedEventCallback | undefined { - availabilityWarn('AudioBufferQueueSourceNode.onEnded', 'web'); - return undefined; - } - - public set onEnded(_callback: OnEndedEventCallback | undefined) { - availabilityWarn('AudioBufferQueueSourceNode.onEnded', 'web'); - } - - public get onPositionChangedInterval(): number { - availabilityWarn( - 'AudioBufferQueueSourceNode.onPositionChangedInterval', - 'web' - ); - return 0; - } - - public set onPositionChangedInterval(_value: number) { - availabilityWarn( - 'AudioBufferQueueSourceNode.onPositionChangedInterval', - 'web' - ); - } - - public get onPositionChanged(): OnPositionChangedEventCallback | undefined { - availabilityWarn('AudioBufferQueueSourceNode.onPositionChanged', 'web'); - return undefined; - } - - public set onPositionChanged( - _callback: OnPositionChangedEventCallback | null - ) { - availabilityWarn('AudioBufferQueueSourceNode.onPositionChanged', 'web'); - } -} +// Maybe someday we will implement this for the web as well +export default AudioBufferQueueSourceNode as unknown as IAudioBufferQueueSourceNode; From 73f566ac42d07b89c7d30d5d040de2ab8df907c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20S=C4=99k?= Date: Thu, 9 Oct 2025 19:18:15 +0200 Subject: [PATCH 17/18] feat: stub for ABBSN on web --- .../src/core/sources/AudioBufferBaseSourceNode.web.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 packages/react-native-audio-api/src/core/sources/AudioBufferBaseSourceNode.web.ts 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; From 265d0538bd595cf69b6bed1333945027d56e4a54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20S=C4=99k?= Date: Mon, 20 Oct 2025 15:14:24 +0200 Subject: [PATCH 18/18] fix: some changes --- .../common-app/src/examples/Record/Record.tsx | 12 +- .../audioapi/system/AudioFocusListener.kt | 27 +-- .../cpp/audioapi/core/utils/AudioDecoder.h | 5 +- .../core/AudioBufferBaseSourceNode.ts | 54 ------ .../core/AudioBufferQueueSourceNode.ts | 50 ----- .../prev_src/core/AudioBufferSourceNode.ts | 121 ------------ .../prev_src/core/AudioRecorder.ts | 4 +- .../prev_src/core/ConstantSourceNode.ts | 13 -- .../prev_src/core/OscillatorNode.ts | 48 ----- .../prev_src/interfaces.ts | 159 +--------------- .../src/core/AudioContext.ts | 26 +++ .../src/core/BaseAudioContext.ts | 46 +++++ .../helpers/createNativeContext.native.ts | 14 ++ .../core/helpers/createNativeContext.web.ts | 5 + .../src/core/helpers/index.ts | 1 + .../AudioBufferQueueSourceNode.native.ts | 2 +- .../sources/AudioBufferSourceNode.native.ts | 49 ++++- .../core/sources/AudioBufferSourceNode.web.ts | 12 +- .../sources/AudioScheduledSourceNode.web.ts | 2 +- .../src/core/sources/CostantSourceNode.ts | 17 ++ .../src/core/sources/OscillatorNode.ts | 11 ++ .../sources/RecorderAdapterNode.native.ts | 21 +++ .../core/sources/RecorderAdapterNode.web.ts | 35 ++++ .../types/generics/IGenericAudioRecorder.ts | 14 ++ .../generics/IGenericRecorderAdapterNode.ts | 6 + .../src/types/generics/index.ts | 2 + .../interfaces/IAudioBufferSourceNode.ts | 6 + .../types/interfaces/IConstantSourceNode.ts | 8 + .../src/types/interfaces/index.ts | 6 +- .../src/types/playground2.ts | 167 ++++++++++++++++ .../src/types/typeUtils.ts | 178 ++++++++++++++++++ 31 files changed, 638 insertions(+), 483 deletions(-) delete mode 100644 packages/react-native-audio-api/prev_src/core/AudioBufferBaseSourceNode.ts delete mode 100644 packages/react-native-audio-api/prev_src/core/AudioBufferQueueSourceNode.ts delete mode 100644 packages/react-native-audio-api/prev_src/core/AudioBufferSourceNode.ts delete mode 100644 packages/react-native-audio-api/prev_src/core/ConstantSourceNode.ts delete mode 100644 packages/react-native-audio-api/prev_src/core/OscillatorNode.ts create mode 100644 packages/react-native-audio-api/src/core/AudioContext.ts create mode 100644 packages/react-native-audio-api/src/core/BaseAudioContext.ts create mode 100644 packages/react-native-audio-api/src/core/helpers/createNativeContext.native.ts create mode 100644 packages/react-native-audio-api/src/core/helpers/createNativeContext.web.ts create mode 100644 packages/react-native-audio-api/src/core/helpers/index.ts create mode 100644 packages/react-native-audio-api/src/core/sources/CostantSourceNode.ts create mode 100644 packages/react-native-audio-api/src/core/sources/RecorderAdapterNode.native.ts create mode 100644 packages/react-native-audio-api/src/core/sources/RecorderAdapterNode.web.ts create mode 100644 packages/react-native-audio-api/src/types/generics/IGenericAudioRecorder.ts create mode 100644 packages/react-native-audio-api/src/types/generics/IGenericRecorderAdapterNode.ts create mode 100644 packages/react-native-audio-api/src/types/interfaces/IConstantSourceNode.ts create mode 100644 packages/react-native-audio-api/src/types/playground2.ts create mode 100644 packages/react-native-audio-api/src/types/typeUtils.ts 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/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/prev_src/core/AudioBufferBaseSourceNode.ts b/packages/react-native-audio-api/prev_src/core/AudioBufferBaseSourceNode.ts deleted file mode 100644 index 4b0a37ae1..000000000 --- a/packages/react-native-audio-api/prev_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/prev_src/core/AudioBufferQueueSourceNode.ts b/packages/react-native-audio-api/prev_src/core/AudioBufferQueueSourceNode.ts deleted file mode 100644 index 13bab8779..000000000 --- a/packages/react-native-audio-api/prev_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/prev_src/core/AudioBufferSourceNode.ts b/packages/react-native-audio-api/prev_src/core/AudioBufferSourceNode.ts deleted file mode 100644 index e8dba4a23..000000000 --- a/packages/react-native-audio-api/prev_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/prev_src/core/AudioRecorder.ts b/packages/react-native-audio-api/prev_src/core/AudioRecorder.ts index 77eedc389..69be78bee 100644 --- a/packages/react-native-audio-api/prev_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/prev_src/core/ConstantSourceNode.ts b/packages/react-native-audio-api/prev_src/core/ConstantSourceNode.ts deleted file mode 100644 index 1a36c4b9f..000000000 --- a/packages/react-native-audio-api/prev_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/prev_src/core/OscillatorNode.ts b/packages/react-native-audio-api/prev_src/core/OscillatorNode.ts deleted file mode 100644 index 48296831c..000000000 --- a/packages/react-native-audio-api/prev_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/prev_src/interfaces.ts b/packages/react-native-audio-api/prev_src/interfaces.ts index 67c5d0299..b4a31032e 100644 --- a/packages/react-native-audio-api/prev_src/interfaces.ts +++ b/packages/react-native-audio-api/prev_src/interfaces.ts @@ -1,12 +1,5 @@ import { AudioEventCallback, AudioEventName } from './events/types'; -import { - BiquadFilterType, - ChannelCountMode, - ChannelInterpretation, - ContextState, - OscillatorType, - WindowType, -} from './types'; +import { ContextState } from './types'; export type WorkletNodeCallback = ( audioData: Array, @@ -90,160 +83,10 @@ export interface IOfflineAudioContext extends IBaseAudioContext { 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 {} diff --git a/packages/react-native-audio-api/src/core/AudioContext.ts b/packages/react-native-audio-api/src/core/AudioContext.ts new file mode 100644 index 000000000..f3bdc38d8 --- /dev/null +++ b/packages/react-native-audio-api/src/core/AudioContext.ts @@ -0,0 +1,26 @@ +import BaseAudioContext, { IBaseAudioContext } from './BaseAudioContext'; + +import { createNativeContext } from './helpers'; + +interface Runtime {} + +/** + * 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; + + constructor() { + const context = createNativeContext(); + super(context as NativeContext); + } +} + +function testFunction() { + const aCtx = new AudioContext(); + + console.log(aCtx.sampleRate); +} diff --git a/packages/react-native-audio-api/src/core/BaseAudioContext.ts b/packages/react-native-audio-api/src/core/BaseAudioContext.ts new file mode 100644 index 000000000..f90cdabd0 --- /dev/null +++ b/packages/react-native-audio-api/src/core/BaseAudioContext.ts @@ -0,0 +1,46 @@ +interface IRecorderAdapterNode {} +interface IOscillatorNode {} + +export interface IBaseAudioContext {} + +interface INativeAudioContext extends IBaseAudioContext { + createMyMomNode(): IRecorderAdapterNode; +} + +interface WebAudioContext + extends IBaseAudioContext, + globalThis.BaseAudioContext {} + +class OscillatorNode { + constructor( + context: BaseAudioContext, + nativeNode: IOscillatorNode + ) { + (context.nativeContext as WebAudioContext).createDynamicsCompressor(); + } +} + +class RecorderAdapterNode { + constructor( + context: BaseAudioContext, + nativeNode: IRecorderAdapterNode + ) {} +} + +export default class BaseAudioContext { + readonly nativeContext: NativeContext; + readonly sampleRate: number; + + constructor(nativeContext: NativeContext) { + this.nativeContext = nativeContext; + this.sampleRate = nativeContext.sampleRate; + } + + createOscillator(): OscillatorNode { + const nativeNode = this.nativeContext.createOscillator(); + + return new OscillatorNode(this, nativeNode); + } + + createRecorderAdapter(): RecorderAdapterNode {} +} 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/sources/AudioBufferQueueSourceNode.native.ts b/packages/react-native-audio-api/src/core/sources/AudioBufferQueueSourceNode.native.ts index d7a80d4b2..95cfde5a5 100644 --- a/packages/react-native-audio-api/src/core/sources/AudioBufferQueueSourceNode.native.ts +++ b/packages/react-native-audio-api/src/core/sources/AudioBufferQueueSourceNode.native.ts @@ -46,7 +46,7 @@ export default class AudioBufferQueueSourceNode< this.node.clearBuffers(); } - public override start(when: number = 0, _offset?: number): void { + public override start(when: number = 0): void { if (when < 0) { throw new RangeError( `when must be a finite non-negative number: ${when}` 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 index 6a6eb9973..820b2c1d0 100644 --- a/packages/react-native-audio-api/src/core/sources/AudioBufferSourceNode.native.ts +++ b/packages/react-native-audio-api/src/core/sources/AudioBufferSourceNode.native.ts @@ -1,10 +1,13 @@ import { InvalidStateError, RangeError } from '../../errors'; -// import { EventEmptyType } from '../../events'; +import { AudioEventSubscription } from '../../events'; import type { IGenericAudioBuffer, IGenericBaseAudioContext, } from '../../types/generics'; -import type { IAudioBufferSourceNode } from '../../types/interfaces'; +import type { + IAudioBufferSourceNode, + LoopEndedEventCallback, +} from '../../types/interfaces'; import AudioBuffer from '../AudioBuffer'; import AudioBufferBaseSourceNode, { NativeAudioBufferBaseSourceNode, @@ -21,6 +24,8 @@ interface NativeAudioBufferSourceNode extends NativeAudioBufferBaseSourceNode { start(when?: number): void; start(when: number, offset: number, duration?: number): void; + + onLoopEnded: string; } export default class AudioBufferSourceNode< @@ -29,16 +34,23 @@ export default class AudioBufferSourceNode< extends AudioBufferBaseSourceNode implements IAudioBufferSourceNode { - public get buffer(): AudioBuffer | null { - if (!this.node.buffer) { - return null; - } + private mBuffer: AudioBuffer | null = null; + private onLoopEndedSubscription?: AudioEventSubscription; + private onLoopEndedCallback?: LoopEndedEventCallback; - return new AudioBuffer(this.node.buffer); + 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 { @@ -100,6 +112,29 @@ export default class AudioBufferSourceNode< 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 { 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 index 50c0e8014..2731c822f 100644 --- a/packages/react-native-audio-api/src/core/sources/AudioBufferSourceNode.web.ts +++ b/packages/react-native-audio-api/src/core/sources/AudioBufferSourceNode.web.ts @@ -4,6 +4,7 @@ import type { ChannelCountMode, ChannelInterpretation } from '../../types'; import type { IGenericBaseAudioContext } from '../../types/generics'; import type { IAudioBufferSourceNode, + LoopEndedEventCallback, OnEndedEventCallback, OnPositionChangedEventCallback, } from '../../types/interfaces'; @@ -357,10 +358,19 @@ export default class AudioBufferSourceNode< this.mCallback = callback; this.mNode.onended = () => { - this.mCallback?.({ bufferId: undefined }); + 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; 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 index 055c61d84..e515db83c 100644 --- a/packages/react-native-audio-api/src/core/sources/AudioScheduledSourceNode.web.ts +++ b/packages/react-native-audio-api/src/core/sources/AudioScheduledSourceNode.web.ts @@ -24,7 +24,7 @@ export default class AudioScheduledSourceNode< } this.node.onended = () => { - this.onEndedCallback?.({ bufferId: undefined }); + this.onEndedCallback?.({ bufferId: undefined, isLast: false }); }; } } 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 index af79b12b9..fefd6a164 100644 --- a/packages/react-native-audio-api/src/core/sources/OscillatorNode.ts +++ b/packages/react-native-audio-api/src/core/sources/OscillatorNode.ts @@ -1,4 +1,5 @@ import { InvalidStateError } from '../../errors'; +import type { EventEmptyType } from '../../events'; import type { OscillatorType } from '../../types'; import type { IGenericBaseAudioContext, @@ -52,4 +53,14 @@ export default class OscillatorNode< 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/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/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/index.ts b/packages/react-native-audio-api/src/types/generics/index.ts index c3a80255a..943806576 100644 --- a/packages/react-native-audio-api/src/types/generics/index.ts +++ b/packages/react-native-audio-api/src/types/generics/index.ts @@ -16,9 +16,11 @@ 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/interfaces/IAudioBufferSourceNode.ts b/packages/react-native-audio-api/src/types/interfaces/IAudioBufferSourceNode.ts index b497277f5..5858faa7a 100644 --- a/packages/react-native-audio-api/src/types/interfaces/IAudioBufferSourceNode.ts +++ b/packages/react-native-audio-api/src/types/interfaces/IAudioBufferSourceNode.ts @@ -1,6 +1,10 @@ +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 { @@ -11,4 +15,6 @@ export default interface IAudioBufferSourceNode< loopEnd: number; start(when?: number, offset?: number, duration?: number): void; + + onLoopEnded: LoopEndedEventCallback | 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 index 3bcadd8f7..129bde21f 100644 --- a/packages/react-native-audio-api/src/types/interfaces/index.ts +++ b/packages/react-native-audio-api/src/types/interfaces/index.ts @@ -11,7 +11,11 @@ export type { OnPositionChangedEventCallback, } from './IAudioBufferBaseSourceNode'; export type { default as IAudioBufferQueueSourceNode } from './IAudioBufferQueueSourceNode'; -export type { default as IAudioBufferSourceNode } from './IAudioBufferSourceNode'; +export type { + default as IAudioBufferSourceNode, + LoopEndedEvent, + LoopEndedEventCallback, +} from './IAudioBufferSourceNode'; export type { default as IAudioScheduledSourceNode, OnEndedEventCallback, 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/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'); + } +}