From 86d29e7411bbc9206108312be6a0c42600b32388 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20S=C4=99k?= Date: Mon, 20 Oct 2025 23:25:26 +0200 Subject: [PATCH 01/33] feat: recording to file - ios dirty --- .../AudioVisualizer/AudioVisualizer.tsx | 10 +- .../common-app/src/examples/Record/Record.tsx | 292 ++++++++++-------- .../FabricExample.xcodeproj/project.pbxproj | 4 +- .../ios/FabricExample/Info.plist | 2 +- apps/fabric-example/ios/Podfile.lock | 2 +- .../audioapi/system/AudioFocusListener.kt | 30 +- .../cpp/audioapi/AudioAPIModuleInstaller.h | 29 +- .../inputs/AudioRecorderHostObject.cpp | 16 +- .../inputs/AudioRecorderHostObject.h | 5 +- .../audioapi/core/inputs/AudioRecorder.cpp | 29 +- .../cpp/audioapi/core/inputs/AudioRecorder.h | 10 + .../ios/audioapi/ios/core/IOSAudioRecorder.h | 17 +- .../ios/audioapi/ios/core/IOSAudioRecorder.mm | 175 ++++++++++- .../audioapi/ios/system/LockScreenManager.h | 1 + .../audioapi/ios/system/LockScreenManager.mm | 25 +- packages/react-native-audio-api/src/api.ts | 61 ++-- .../src/core/AudioRecorder.ts | 10 +- .../react-native-audio-api/src/interfaces.ts | 2 +- packages/react-native-audio-api/src/types.ts | 8 +- 19 files changed, 498 insertions(+), 230 deletions(-) diff --git a/apps/common-app/src/examples/AudioVisualizer/AudioVisualizer.tsx b/apps/common-app/src/examples/AudioVisualizer/AudioVisualizer.tsx index 50fefe1c6..655077030 100644 --- a/apps/common-app/src/examples/AudioVisualizer/AudioVisualizer.tsx +++ b/apps/common-app/src/examples/AudioVisualizer/AudioVisualizer.tsx @@ -1,15 +1,15 @@ -import React, { useState, useEffect, useRef } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; +import { ActivityIndicator, View } from 'react-native'; import { - AudioContext, AnalyserNode, AudioBuffer, AudioBufferSourceNode, + AudioContext, } from 'react-native-audio-api'; -import { ActivityIndicator, View } from 'react-native'; -import FreqTimeChart from './FreqTimeChart'; -import { Container, Button } from '../../components'; +import { Button, Container } from '../../components'; import { layout } from '../../styles'; +import FreqTimeChart from './FreqTimeChart'; const FFT_SIZE = 512; diff --git a/apps/common-app/src/examples/Record/Record.tsx b/apps/common-app/src/examples/Record/Record.tsx index 78e220a85..11f5b1c86 100644 --- a/apps/common-app/src/examples/Record/Record.tsx +++ b/apps/common-app/src/examples/Record/Record.tsx @@ -1,147 +1,53 @@ -import React, { useRef, FC, useEffect } from 'react'; +import React, { FC, useEffect, useMemo, useRef, useState } 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; +const SAMPLE_RATE = 48000; const Record: FC = () => { - const recorderRef = useRef(null); - const aCtxRef = useRef(null); - const recorderAdapterRef = useRef(null); - const audioBuffersRef = useRef([]); - const sourcesRef = useRef([]); + const [isRecording, setIsRecording] = useState(false); - useEffect(() => { - const setup = async () => { - await AudioManager.requestRecordingPermissions(); - recorderRef.current = new AudioRecorder({ - sampleRate: SAMPLE_RATE, - bufferLengthInSamples: SAMPLE_RATE, - }); - }; + // const audioContext = useMemo(() => { + // return new AudioContext({ sampleRate: SAMPLE_RATE, initSuspended: true }); + // }, []); - setup(); - return () => { - aCtxRef.current?.close(); - stopRecorder(); - }; - }, []); + const recorder = useMemo( + () => + new AudioRecorder({ + sampleRate: SAMPLE_RATE, + bufferLengthInSamples: 2048, + recordToFile: true, + fileDirectory: 'Document', + }), + [] + ); - const setupRecording = () => { + const onStartRecording = async () => { AudioManager.setAudioSessionOptions({ iosCategory: 'playAndRecord', iosMode: 'spokenAudio', iosOptions: ['defaultToSpeaker', 'allowBluetoothA2DP'], }); - }; - const stopRecorder = () => { - if (recorderRef.current) { - recorderRef.current.stop(); - console.log('Recording stopped'); - // advised, but not required - AudioManager.setAudioSessionOptions({ - iosCategory: 'playback', - iosMode: 'default', - }); - } else { - console.error('AudioRecorder is not initialized'); - } - }; - - const startEcho = () => { - if (!recorderRef.current) { - console.error('AudioContext or AudioRecorder is not initialized'); - return; - } - setupRecording(); + await AudioManager.setAudioSessionActivity(true); - aCtxRef.current = new AudioContext({ sampleRate: SAMPLE_RATE }); - recorderAdapterRef.current = aCtxRef.current.createRecorderAdapter(); - recorderAdapterRef.current.connect(aCtxRef.current.destination); - recorderRef.current.connect(recorderAdapterRef.current); - - recorderRef.current.start(); - console.log('Recording started'); - console.log('Audio context state:', aCtxRef.current.state); - if (aCtxRef.current.state === 'suspended') { - console.log('Resuming audio context'); - aCtxRef.current.resume(); - } + recorder.start(); + setIsRecording(true); }; - /// This stops only the recording, not the audio context - const stopEcho = () => { - stopRecorder(); - aCtxRef.current = null; - recorderAdapterRef.current = null; - }; - - const startRecordReplay = () => { - if (!recorderRef.current) { - console.error('AudioRecorder is not initialized'); - return; - } - setupRecording(); - audioBuffersRef.current = []; - - recorderRef.current.onAudioReady((event) => { - const { buffer, numFrames } = event; - - console.log('Audio recorder buffer ready:', buffer.duration, numFrames); - audioBuffersRef.current.push(buffer); - }); - - recorderRef.current.start(); - - setTimeout(() => { - stopRecorder(); - }, 5000); - }; - - const stopRecordReplay = () => { - const aCtx = new AudioContext({ sampleRate: SAMPLE_RATE }); - aCtxRef.current = aCtx; - - if (aCtx.state === 'suspended') { - aCtx.resume(); - } - - const tNow = aCtx.currentTime; - let nextStartAt = tNow + 1; - const buffers = audioBuffersRef.current; - - console.log(tNow, nextStartAt, buffers.length); - - for (let i = 0; i < buffers.length; i++) { - const source = aCtx.createBufferSource(); - source.buffer = buffers[i]; - - source.connect(aCtx.destination); - sourcesRef.current.push(source); - - source.start(nextStartAt); - nextStartAt += buffers[i].duration; - } - - setTimeout( - () => { - console.log('clearing data'); - audioBuffersRef.current = []; - sourcesRef.current = []; - }, - (nextStartAt - tNow) * 1000 - ); + const onStopRecording = () => { + setIsRecording(false); + recorder.stop(); }; return ( @@ -150,19 +56,147 @@ const Record: FC = () => { Sample rate: {SAMPLE_RATE} - Echo -