diff --git a/.claude/skills/audio-nodes/SKILL.md b/.claude/skills/audio-nodes/SKILL.md index 39300f21c..7d0b90f86 100644 --- a/.claude/skills/audio-nodes/SKILL.md +++ b/.claude/skills/audio-nodes/SKILL.md @@ -141,7 +141,6 @@ classDiagram AudioNode <|-- WorkletNode AudioNode <|-- AnalyserNode AudioNode <|-- AudioDestinationNode - AudioNode <|-- AudioRecorder AudioScheduledSourceNode <|-- AudioBufferBaseSourceNode AudioScheduledSourceNode <|-- OscillatorNode @@ -152,6 +151,25 @@ classDiagram AudioBufferBaseSourceNode <|-- AudioBufferQueueSourceNode ``` +### AudioRecorder (not an AudioNode) + +`core/inputs/AudioRecorder` is a standalone base class, not part of the `AudioNode` hierarchy — it +feeds the graph through a `RecorderAdapterNode` instead of being processed by it. + +The split between it and `IOSAudioRecorder` / `AndroidAudioRecorder` is: the base owns everything +that happens to recorded frames (file writer, JS callback, adapter node — `enableFileOutput`, +`setupFileWriter`, `setOnAudioReadyCallback`, `connect`, `detachSideEffects`/`finalizeSideEffects`), the +subclasses own only the platform input stream. The one thing the base needs from the platform is +`resolveStreamFormat()`, returning sample rate, channel count and max frames per buffer; iOS reads +it from `NativeAudioRecorder` on every call (a route change invalidates it), Android returns values +cached when the Oboe stream opened. Add shared recorder behavior to the base, not to one platform. + +Pitfall: never redeclare a base member (`deinterleavingBuffer_`, `streamSampleRate_`, +`recordingSegmentPaths_`) in a platform recorder. The shadowing copy compiles fine, but the base's +audio-thread fan-out reads its own member and silently drops that output. + +--- + ### AudioScheduledSourceNode (internal only — not exposed to JS directly) Base class for source nodes that have a scheduled start and stop time. **Not instantiated directly.** diff --git a/.claude/skills/build-compilation-dependencies/SKILL.md b/.claude/skills/build-compilation-dependencies/SKILL.md index d7cc4202f..5e7635d64 100644 --- a/.claude/skills/build-compilation-dependencies/SKILL.md +++ b/.claude/skills/build-compilation-dependencies/SKILL.md @@ -31,6 +31,7 @@ react-native-audio-api/ │ │ └── CMakeLists.txt # Actual Android C++ build target │ ├── common/cpp/audioapi/ # Shared C++ (used by all platforms) │ │ ├── decoding/ # Decoder factory, backends, SeekDecoderDaemon, AudioDecoding, AudioFileConcatenator +│ │ ├── encoding/ # AudioEncoder interface, EncoderCapabilities, OS encoder/remux selector headers │ │ ├── libs/ # Third-party wrappers (FFmpeg, miniaudio, pffft, …) │ │ └── external/ # Prebuilt binaries per platform │ │ ├── android/ # .a static libs (Opus, Ogg, Vorbis, OpenSSL) @@ -315,6 +316,7 @@ CI runs a parallel `cpp-coverage` job via `.github/workflows/cpp-coverage-job.ym - Compile definitions: `RN_AUDIO_API_ENABLE_WORKLETS=0`, `RN_AUDIO_API_TEST=1`, `RN_AUDIO_API_FFMPEG_DISABLED=1` - Google Test auto-fetched via `FetchContent` if not installed locally - New test files in `test/src/**/*.cpp` are picked up automatically by glob — no CMakeLists edit needed +- `jsi.cpp` is compiled into the static lib so library members that reference JSI symbols (e.g. `AudioFileProperties::CreateFromJSIValue`) link when a test first pulls them in; a static-lib member costs nothing unless demanded. If a new test triggers `Undefined symbols: facebook::jsi::...`, the referenced runtime source is missing from the lib — add it there rather than stubbing the symbol For `MockAudioEventHandlerRegistry`, `TestableXxx` pattern, and full CMakeLists analysis see [build-details.md](build-details.md#c-test-build--commoncpptestcmakeliststxt--detailed-analysis). @@ -353,6 +355,8 @@ Resolution pitfalls learned the hard way (both handled inside `package-root.js`) | `HAVE_ACCELERATE` | Not set | `GCC_PREPROCESSOR_DEFINITIONS` | Not set | | `RN_AUDIO_API_TEST` | Not set | Not set | Always set to 1 | +**OS-API selector headers** (`decoding/OSDecoding.h`, `encoding/OSEncoding.h`, `encoding/OSRemux.h`, `encoding/OSFilePath.h`): common code reaches platform implementations through `#if defined(__ANDROID__)` / `#elif defined(__APPLE__) && !defined(RN_AUDIO_API_TEST) && !defined(RN_AUDIO_API_NODE)` dispatch. The Apple branch must exclude **both** desktop defines: the gtest build (`RN_AUDIO_API_TEST`) and the WPT node addon (`RN_AUDIO_API_NODE`) run on macOS (where `__APPLE__` is defined) but do not compile or link the `ios/` ObjC++ sources. The node build cannot borrow `RN_AUDIO_API_TEST` instead — that flag also switches on gtest-only code (`gtest_prod.h` includes, test `ArrayBuffer` shims). When adding a new OS-selector header, copy the full three-clause guard; an incremental `wpt_tests/build` dir can mask a missing clause for a long time, so verify with a clean `yarn node:build`. Platform glue selected this way lives in `android/src/main/cpp/audioapi/android/` (e.g. `AndroidDecoding`, `AndroidEncoder`, `AndroidRemux`) and `ios/audioapi/ios/core/utils/` (e.g. `IOSDecoding`, `IOSEncoder`, `IOSRemux`) — both picked up automatically by the CMake glob / podspec glob, no build-file edits needed. + --- ## Common Build Failure Patterns @@ -367,6 +371,7 @@ Resolution pitfalls learned the hard way (both handled inside `package-root.js`) | New `.cpp` not compiled in tests | Glob picks it up automatically — may need cmake reconfigure | Delete `test/build/` and re-run | | iOS compile error `unknown type 'id'` | C++ file included ObjC-only header | Compile that file as ObjC++ (separate subspec with `-x objective-c++`) | | `RCT_NEW_ARCH_ENABLED` undefined on Android | Old RN gradle plugin | Ensure `newArchEnabled=true` in app's `gradle.properties` | +| iOS: `'to_chars' is unavailable: introduced in iOS 16.3` from `formatter_floating_point.h`, instantiated by `std::format<...>` | `std::format` in code compiled for iOS. libc++ availability-gates the whole `` library to iOS 16.3; the podspec minimum is `ios_min_version = '14.0'`. The desktop C++ test build and Android NDK have no such gate, so `yarn test:cpp` passes and only the iOS build fails. | Use `std::string` concatenation / `std::to_string` in `common/cpp` and `ios/`. Zero-pad by hand (`insert(0, n, '0')`). Android-only files (`android/src/main/cpp`) may keep `std::format`. | --- diff --git a/.claude/skills/post-work-checks/SKILL.md b/.claude/skills/post-work-checks/SKILL.md index 7433eda7e..7f35d9c30 100644 --- a/.claude/skills/post-work-checks/SKILL.md +++ b/.claude/skills/post-work-checks/SKILL.md @@ -123,13 +123,13 @@ yarn test # from monorepo root — runs test:js + test:cpp **When**: after any change to C++ files or TypeScript files in `src/`. Prefer this for a quick local test loop covering both TS and C++ logic; run `yarn validate:fast` before opening a PR. -### AudioEvent enum sync check +### Enum sync check ```bash yarn check-audio-enum-sync ``` -**When**: only when you modify the `AudioEvent` enum or any file that maps event names across C++/Kotlin/TypeScript. Skip this step if you already ran `validate:fast` (it includes enum sync). +**When**: when you modify `AudioEvent`, `FileFormat` / `AudioFileProperties::Format`, or other JSI-crossing recorder enums (`FileDirectory`, `BitDepth`, `IOSAudioQuality`). Skip if you already ran `validate:fast` (it includes enum sync). --- diff --git a/.claude/skills/post-work-checks/maintenance.md b/.claude/skills/post-work-checks/maintenance.md index b4863c5e2..35a6b7db6 100644 --- a/.claude/skills/post-work-checks/maintenance.md +++ b/.claude/skills/post-work-checks/maintenance.md @@ -10,5 +10,5 @@ Review this skill when `pre-push-update` reports changes in: | `packages/react-native-audio-api/package.json` scripts | Package-level command changes (including per-language lint/format) | | `lefthook.yml` | Pre-commit / commit-msg hook changes | | `scripts/validate.sh` | Tier behavior (`--fast` / `--cpp-extended` / `--android` / `--ios` / `--full`), skip rules | -| `scripts/check-audio-enum-sync*` or `packages/react-native-audio-api/scripts/check-audio-events-sync.sh` | Enum sync check details | +| `scripts/check-audio-enum-sync*` or `packages/react-native-audio-api/scripts/check-*-enum-sync.sh` / `check-enum-sync.sh` | Enum sync check details (AudioEvent + AudioFileProperties) | | `.github/workflows/ci.yml`, `tests.yml`, `cpp-job.yml` | What CI covers vs local validation tiers | diff --git a/.claude/skills/thread-safety-itc/SKILL.md b/.claude/skills/thread-safety-itc/SKILL.md index 83a029480..5a9ace6bf 100644 --- a/.claude/skills/thread-safety-itc/SKILL.md +++ b/.claude/skills/thread-safety-itc/SKILL.md @@ -149,7 +149,25 @@ offloader.scheduleTask(std::move(workItem)); See the `utilities` skill for full API. -**Pitfall — file writer / recorder shutdown:** `TaskOffloader::shutdown()` drains the SPSC queue before joining the worker. Call it (or destroy the offloader) only after `isFileOpen_` is cleared so the audio thread stops enqueueing. Otherwise rotated or closed M4A segments lose seconds of buffered audio. Types with a `.slot` member use `slot == size_t max` as the shutdown sentinel. +**Pitfall — file writer / recorder shutdown:** `TaskOffloader::shutdown()` drains the SPSC queue before joining the worker, and it drains by *running the task* for each pending item. So drain **while the file still counts as open**. `runWriterTask()` gates its encode on `isFileOpen_`, so clearing that flag first makes every drained buffer take the false branch and be dropped, costing a rotated or closed M4A segment seconds of buffered audio. `finishCurrentFile()` therefore destroys the offloader first and clears the flag after, not the other way round. Types with a `.slot` member use `slot == size_t max` as the shutdown sentinel. + +That flag is **not** what keeps the audio thread out of the pool being freed, and it cannot be: `writeAudioData()` reads it and then dereferences the pool pointers, so no ordering of those two lines closes the window between the two steps. The caller closes it. The audio thread only enters the writer under the recorder's `fileWriterMutex_`, and every close path either moves the writer out of `fileWriter_` under that mutex first (`disableFileOutput`, `detachSideEffects`) or holds it across the call (the iOS format-change path). That includes the platform recorder destructors: both call `stop()` first, which detaches under the mutexes, and only then close the native stream. A writer driven without that discipline would need a guard of its own. + +**Android ownership cycle.** `AndroidAudioRecorder::openAudioStream()` hands Oboe `shared_from_this()`, and Oboe keeps that `shared_ptr` for the lifetime of the stream *object* (`AudioStreamBase::mSharedDataCallback`), not just while the stream is open. Since `mStream_` is a member, an opened recorder owns itself through its stream until `cleanup()` resets `mStream_`. Two consequences. The destructor can only run after `cleanup()` has already closed the stream, so no callback can race it — which is why the pre-`stop()` destructor got away with an unlocked `closeFile()`. And `stop()` only calls `requestStop()`, never `cleanup()`, so once JS drops a normally stopped recorder nothing breaks the cycle: the recorder and its AAudio stream leak until a disconnect error, and a recorder dropped mid-recording keeps recording. Any change that breaks the cycle (closing in `stop()`, releasing from the HostObject, or a weak-pointer proxy as Oboe's callback) makes the destructor order above load-bearing. + +Never hold a lock the worker takes while joining it: `finishCurrentFile()` drains and joins, and only then takes `fileMutex_` to retire the encoder — otherwise the join deadlocks against whatever the worker is doing under that lock. + +**Pattern — reacting to audio without blocking on it.** When a decision depends on data the audio thread produced but costs blocking work to make (rotating a recording once its file outgrows a cap), do not make it in `writeAudioData()`. Measuring a file can be a `stat`, and acting on it closes and opens an encoder — both forbidden on the audio thread. `AudioFileWriter` makes that decision **on its own worker thread**, in the task that just encoded a buffer (`rotateOnceFileOutgrowsCap()`), and only when the file properties carry a non-zero `rotateIntervalBytes`, so a session that never rotates never pays for the `stat`. One mutex (`fileMutex_`) guards the encoder together with the session bookkeeping the rotation advances (file count, finished-file totals); the JS thread reads those under the same lock. Anything that reaches back out of the writer — the file-opened callback the recorder uses to collect segment paths, the error event — is invoked **after** that lock is released, so a callback may call straight back in (`getFilePath()`) without re-entering a non-recursive mutex. The join-versus-lock ordering is the shutdown pitfall above: `finishCurrentFile()` stops the worker first, then takes the lock to retire the encoder. + +Swapping the output file must not restart the worker: a rotation exchanges the encoder underneath the live offloader, so a rotating recording costs one thread per file open, not one per segment. Only `reprepareStreamFormat()` (a stream-format change) stops the worker, because the buffer pool is sized from the format. + +**Test seam — inject, do not derive.** `resolveOsFilePath` and `createOsEncoder` both fail in the desktop test build, so a real writer cannot open a file there. Those two steps are bundled into a `PlatformFileBackend` struct of two `std::function`s, passed to the writer's constructor and defaulting to `createOsFileBackend()`. The test builds a backend returning bare file names and a fake `AudioEncoder`; everything else — pool, worker, rotation, totals — runs for real, on a real `AudioFileWriter` rather than a subclass. + +Prefer this shape over `protected virtual` hooks for a class the production code instantiates directly. Virtual hooks make the class polymorphic, which then forces a virtual destructor for anyone holding it by base pointer, and they let a test assert against a subclass instead of the real type. With injection the writer is `final` with a non-virtual destructor, and a misconstructed backend is the one thing to guard: calling an empty `std::function` throws, and on the worker thread that terminates, so `openEncoderForNextFile()` checks both slots and returns an error instead. + +Tests stay deterministic by writing at most a pool's worth of buffers (32) per open and asserting only after `closeFile()`, which drains and joins the worker. + +**Pitfall — the task type cannot be a nested struct.** `TaskOffloader` constrains `T` with `std::default_initializable`. A task struct carrying default member initializers (which the `.slot` sentinel requires) does *not* satisfy that constraint while its enclosing class is still incomplete, so `using Offloader = TaskOffloader;` inside the class fails to compile with "constraints not satisfied". Making the struct `public` does not help — it is not an access problem. Declare the task type at **namespace scope** instead (`PendingFileWrite`, `PendingCallbackFrames`). Dropping the initializers to satisfy the constraint is worse: `T{}` would then produce `slot == 0`, a valid slot index, making the shutdown sentinel indistinguishable from real work. --- @@ -231,6 +249,7 @@ suspend.then→suspended, event→suspended`. - **Copying `shared_ptr` inside `processNode()`** — increments atomic refcount; capture before entering hot path. - **Locking `initialize()` or graph factory methods** — `initialize()` runs synchronously during HostObject construction on the JS thread; node factories and `createMediaElementSource()` are synchronous JS calls. Only lifecycle methods that touch the driver or offline render thread need `driverMutex_`. - **Locking only `AudioContext`** — iOS recorder, session, and interruption paths mutate the shared `AVAudioEngine` outside `AudioContext`; keep the `AudioEngine` mutex on those entry points. Offline render uses the same `driverMutex_` on `BaseAudioContext`. +- **Duplicating recorder fan-out in platform code** — `AudioRecorder::onAudioFrames(interleavedFrames, numFrames)` (base class, `common/cpp/audioapi/core/inputs/`) is the single audio-thread fan-out to file writer, JS callback, and adapter node, using tryLock-and-drop per consumer. Platform recorders (e.g. `IOSAudioRecorder`) only normalize the platform buffer to interleaved float32 and call it — adding per-consumer writes in the platform receiver block double-writes every buffer. The interleave config (`inputChannelCount_`, scratch buffer) is read unlocked by the audio thread, so it may only be mutated while the input is disarmed (start/stop/input-format-change paths). - **Re-entering `driverMutex_` or the `AudioEngine` mutex on the same thread** — call `tryStartDriver()` directly from `resume()` instead of `start()`; use lock-free `isStreamRunning()` from `isDriverRunning()`. `AudioContext::start()` does not acquire `driverMutex_`; it asserts the lock is already held when the driver is not initialized (via `scheduleAudioEvent` synchronous path). When already initialized, `start()` is a lock-free no-op so `source.start()` on the audio thread does not take the mutex. --- diff --git a/.claude/skills/thread-safety-itc/maintenance.md b/.claude/skills/thread-safety-itc/maintenance.md index b3b16dd31..0e06742d9 100644 --- a/.claude/skills/thread-safety-itc/maintenance.md +++ b/.claude/skills/thread-safety-itc/maintenance.md @@ -11,4 +11,5 @@ Review this skill when `pre-push-update` reports changes in: | `common/cpp/audioapi/utils/CrossThreadEventScheduler.hpp` | Scheduler API changes — update decision table | | `common/cpp/audioapi/core/AudioNode.*` | Audio thread contract changes | | `common/cpp/audioapi/core/utils/AudioGraphManager.*` | Graph mutation queue changes | +| `common/cpp/audioapi/core/utils/AudioFileWriter.*` | The "reacting to audio without blocking on it" pattern: rotation decided in the worker task, the callbacks-outside-the-lock rule, the never-join-while-holding rule, the injected `PlatformFileBackend` test seam | | Any new cross-thread primitive in `utils/` | Document in the decision table | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1fa0a3fb1..ce859aadf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,7 @@ jobs: check-audio-enum-sync: uses: ./.github/workflows/ci-check.yml with: - name: Check AudioEvent enum sync + name: Check enum sync run: yarn check-audio-enum-sync build-audio-api: diff --git a/apps/common-app/src/demos/Record/Record.tsx b/apps/common-app/src/demos/Record/Record.tsx index d6c4d0acd..24706f2de 100644 --- a/apps/common-app/src/demos/Record/Record.tsx +++ b/apps/common-app/src/demos/Record/Record.tsx @@ -20,6 +20,16 @@ import RecordingVisualization from './RecordingVisualization'; import Status from './Status'; import { RecordingState } from './types'; +// concatAudioFiles supports WAV, M4A, and FLAC — the formats recordable on +// both iOS and Android. +const RECORDING_EXTENSION = FileFormat.M4A; +const ROTATING_SIZE = 250_000; + +const RECORDING_EXTENSION_NAME_MAP = { + [FileFormat.Wav]: 'wav', + [FileFormat.M4A]: 'm4a', + [FileFormat.Flac]: 'flac', +}; const Record: FC = () => { const [state, setState] = useState(RecordingState.Idle); const [hasPermissions, setHasPermissions] = useState(false); @@ -90,9 +100,7 @@ const Record: FC = () => { return; } - const result = await Recorder.start({ - fileNameOverride: `overridden_name_${Date.now()}`, - }); + const result = await Recorder.start(); setupNotification(false); @@ -130,9 +138,14 @@ const Record: FC = () => { return; } - const outputPath = info.paths[0].replace(/[^/]+$/, 'recording.m4a'); + const extension = RECORDING_EXTENSION_NAME_MAP[RECORDING_EXTENSION]; + const outputPath = info.paths[0].replace( + /[^/]+$/, + `recording.${extension}` + ); const finalPath = await concatAudioFiles(info.paths, outputPath); + // const finalPath = info.paths[0]; const audioBuffer = await audioContext.decodeAudioData(finalPath); setRecordedBuffer(audioBuffer); @@ -262,7 +275,11 @@ const Record: FC = () => { }, [onPauseRecording, onResumeRecording]); useEffect(() => { - Recorder.enableFileOutput({ rotateIntervalBytes: 1_000_000, format: FileFormat.M4A }); + Recorder.enableFileOutput({ + rotateIntervalBytes: ROTATING_SIZE, + format: RECORDING_EXTENSION, + fileName: 'my_recording' + }); return () => { stopPlayback(); diff --git a/apps/common-app/src/other/AudioPipelineStress/AudioPipelineStress.tsx b/apps/common-app/src/other/AudioPipelineStress/AudioPipelineStress.tsx index 98740874b..16c95cc18 100644 --- a/apps/common-app/src/other/AudioPipelineStress/AudioPipelineStress.tsx +++ b/apps/common-app/src/other/AudioPipelineStress/AudioPipelineStress.tsx @@ -281,13 +281,11 @@ const AudioPipelineStress: FC = () => { } }; - const performCleanRecording = async ( - fileNameOverride: string - ): Promise => { + const performCleanRecording = async (): Promise => { await activateRecordingSession(); resourcesRef.current.configureRecorderTap(); - await resourcesRef.current.startRecording(fileNameOverride); + await resourcesRef.current.startRecording(); await waitForRecordingCallbacks(1); await sleep(SHORT_RECORDING_MS); @@ -327,7 +325,7 @@ const AudioPipelineStress: FC = () => { }; const performCleanRecordPlaybackCycle = async (label: string) => { - const capture = await performCleanRecording(`${label}-${Date.now()}`); + const capture = await performCleanRecording(); await performCleanPlayback( capture.decodedBuffer, Math.min(capture.decodedBuffer.duration, 1.4), @@ -474,9 +472,7 @@ const AudioPipelineStress: FC = () => { 'record-and-decode', 'Record briefly, then decode output', async () => { - const capture = await performCleanRecording( - `record-warmup-${Date.now()}` - ); + const capture = await performCleanRecording(); addInfoStep( steps, 'recorded-file', @@ -499,9 +495,7 @@ const AudioPipelineStress: FC = () => { `cycle-${cycle}`, `Cycle ${cycle}: record, decode, and play recorded audio`, async () => { - const capture = await performCleanRecording( - `record-to-playback-${cycle}-${Date.now()}` - ); + const capture = await performCleanRecording(); const playbackStats = await performCleanPlayback( capture.decodedBuffer, @@ -551,9 +545,7 @@ const AudioPipelineStress: FC = () => { `Cycle ${cycle} pre-record playback engine timing`, formatPlaybackProgressStats(playbackStats) ); - await performCleanRecording( - `playback-to-record-${cycle}-${Date.now()}` - ); + await performCleanRecording(); } ); } @@ -691,7 +683,7 @@ const AudioPipelineStress: FC = () => { 'clean-recovery-cycle', 'Run one clean record and decode cycle after recovery', async () => { - await performCleanRecording(`post-record-recovery-${Date.now()}`); + await performCleanRecording(); } ); } @@ -716,9 +708,7 @@ const AudioPipelineStress: FC = () => { await AudioManager.setAudioSessionActivity(true); resourcesRef.current.configureRecorderTap(); - const result = await resourcesRef.current.tryStartRecording( - `wrong-category-${Date.now()}` - ); + const result = await resourcesRef.current.tryStartRecording(); if (result.status === 'success') { throw new Error( @@ -743,9 +733,7 @@ const AudioPipelineStress: FC = () => { 'clean-recovery-record', 'Switch back to playAndRecord and confirm clean recording works', async () => { - await performCleanRecording( - `wrong-category-recovery-${Date.now()}` - ); + await performCleanRecording(); } ); } diff --git a/apps/common-app/src/other/AudioPipelineStress/StressResourceOwner.ts b/apps/common-app/src/other/AudioPipelineStress/StressResourceOwner.ts index af0f293e3..0903f7a44 100644 --- a/apps/common-app/src/other/AudioPipelineStress/StressResourceOwner.ts +++ b/apps/common-app/src/other/AudioPipelineStress/StressResourceOwner.ts @@ -48,7 +48,7 @@ export default class StressResourceOwner { const fileOutputResult = recorder.enableFileOutput({ channelCount: 1, directory: FileDirectory.Cache, - fileNamePrefix: 'audio-pipeline-stress', + fileName: 'audio-pipeline-stress', format: FileFormat.M4A, subDirectory: 'AudioPipelineStress', }); @@ -115,18 +115,18 @@ export default class StressResourceOwner { } } - async startRecording(fileNameOverride: string): Promise { + async startRecording(): Promise { const { recorder } = this.getReadyResources(); - const result = await recorder.start({ fileNameOverride }); + const result = await recorder.start(); if (result.status === 'error') { throw new Error(`Failed to start recording: ${result.message}`); } } - tryStartRecording(fileNameOverride: string) { + tryStartRecording() { const { recorder } = this.getReadyResources(); - return recorder.start({ fileNameOverride }); + return recorder.start(); } async stopRecordingAndDecode(): Promise { diff --git a/apps/fabric-example/ios/FabricExample.xcodeproj/project.pbxproj b/apps/fabric-example/ios/FabricExample.xcodeproj/project.pbxproj index e8340e9ff..5dc75c0ff 100644 --- a/apps/fabric-example/ios/FabricExample.xcodeproj/project.pbxproj +++ b/apps/fabric-example/ios/FabricExample.xcodeproj/project.pbxproj @@ -8,8 +8,9 @@ /* Begin PBXBuildFile section */ 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; + 4169E7BEA9F7E99D2BE7625F /* libPods-FabricExampleTestHost.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9C7A65B861FEEF104BF68E4A /* libPods-FabricExampleTestHost.a */; }; + 6FCBD9016D95736D685FE800 /* libPods-FabricExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1567B6CDDE90682EB57FEB33 /* libPods-FabricExample.a */; }; 761780ED2CA45674006654EE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761780EC2CA45674006654EE /* AppDelegate.swift */; }; - 782077328032E706EB7A3E71 /* libPods-FabricExampleTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C7604CA8A8034C4311483555 /* libPods-FabricExampleTests.a */; }; 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; 88485BE83AA320DC6673875F /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */; }; A1B2C3D41E00000100A0A001 /* AudioEngineTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D61E00000100A0A001 /* AudioEngineTests.mm */; }; @@ -20,8 +21,7 @@ A1B2C4201E00000100A0A001 /* IOSAudioRecorderTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C4211E00000100A0A001 /* IOSAudioRecorderTests.mm */; }; A1B2C4301E00000100A0A001 /* AudioAPIModuleTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C4311E00000100A0A001 /* AudioAPIModuleTests.mm */; }; B2C3D541E00000100A0A001 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2C3D511E00000100A0A001 /* AppDelegate.swift */; }; - C1BA078C772122BE5F5984EE /* libPods-FabricExampleTestHost.a in Frameworks */ = {isa = PBXBuildFile; fileRef = DC009E8601127E4B5DB8C238 /* libPods-FabricExampleTestHost.a */; }; - F2EF6EA995C3D07711BB3E99 /* libPods-FabricExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = EF53ABF265599E275947FD77 /* libPods-FabricExample.a */; }; + D651FCF536FBFFFB277E52A4 /* libPods-FabricExampleTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 603B62CE61243E0661D1C698 /* libPods-FabricExampleTests.a */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -39,10 +39,15 @@ 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = FabricExample/Images.xcassets; sourceTree = ""; }; 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = FabricExample/Info.plist; sourceTree = ""; }; 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = PrivacyInfo.xcprivacy; path = FabricExample/PrivacyInfo.xcprivacy; sourceTree = ""; }; - 3C8DEB077790086AF6400A95 /* Pods-FabricExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FabricExample.debug.xcconfig"; path = "Target Support Files/Pods-FabricExample/Pods-FabricExample.debug.xcconfig"; sourceTree = ""; }; - 75EABA3D2424D85D2A417C44 /* Pods-FabricExampleTestHost.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FabricExampleTestHost.release.xcconfig"; path = "Target Support Files/Pods-FabricExampleTestHost/Pods-FabricExampleTestHost.release.xcconfig"; sourceTree = ""; }; + 1567B6CDDE90682EB57FEB33 /* libPods-FabricExample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-FabricExample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 1C9B8045747B6E85492A2647 /* Pods-FabricExampleTestHost.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FabricExampleTestHost.release.xcconfig"; path = "Target Support Files/Pods-FabricExampleTestHost/Pods-FabricExampleTestHost.release.xcconfig"; sourceTree = ""; }; + 4ED195C5A18E086D371F67C8 /* Pods-FabricExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FabricExample.debug.xcconfig"; path = "Target Support Files/Pods-FabricExample/Pods-FabricExample.debug.xcconfig"; sourceTree = ""; }; + 56E271DBAE1677D0B8FEE0D8 /* Pods-FabricExampleTestHost.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FabricExampleTestHost.debug.xcconfig"; path = "Target Support Files/Pods-FabricExampleTestHost/Pods-FabricExampleTestHost.debug.xcconfig"; sourceTree = ""; }; + 603B62CE61243E0661D1C698 /* libPods-FabricExampleTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-FabricExampleTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 761780EC2CA45674006654EE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = FabricExample/AppDelegate.swift; sourceTree = ""; }; 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = FabricExample/LaunchScreen.storyboard; sourceTree = ""; }; + 97F18A960BF8590E4768681F /* Pods-FabricExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FabricExample.release.xcconfig"; path = "Target Support Files/Pods-FabricExample/Pods-FabricExample.release.xcconfig"; sourceTree = ""; }; + 9C7A65B861FEEF104BF68E4A /* libPods-FabricExampleTestHost.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-FabricExampleTestHost.a"; sourceTree = BUILT_PRODUCTS_DIR; }; A1B2C3D51E00000100A0A001 /* FabricExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FabricExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; A1B2C3D61E00000100A0A001 /* AudioEngineTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = AudioEngineTests.mm; sourceTree = ""; }; A1B2C3E11E00000100A0A001 /* AudioSessionManagerTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = AudioSessionManagerTests.mm; sourceTree = ""; }; @@ -51,17 +56,12 @@ A1B2C4111E00000100A0A001 /* NativeAudioRecorderTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = NativeAudioRecorderTests.mm; sourceTree = ""; }; A1B2C4211E00000100A0A001 /* IOSAudioRecorderTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = IOSAudioRecorderTests.mm; sourceTree = ""; }; A1B2C4311E00000100A0A001 /* AudioAPIModuleTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = AudioAPIModuleTests.mm; sourceTree = ""; }; - A3B9705E574724C4530FA170 /* Pods-FabricExampleTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FabricExampleTests.debug.xcconfig"; path = "Target Support Files/Pods-FabricExampleTests/Pods-FabricExampleTests.debug.xcconfig"; sourceTree = ""; }; B2C3D501E00000100A0A001 /* FabricExampleTestHost.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FabricExampleTestHost.app; sourceTree = BUILT_PRODUCTS_DIR; }; B2C3D511E00000100A0A001 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; B2C3D521E00000100A0A001 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - C7604CA8A8034C4311483555 /* libPods-FabricExampleTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-FabricExampleTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - C7B9E83E1B54A0E19548A55D /* Pods-FabricExampleTestHost.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FabricExampleTestHost.debug.xcconfig"; path = "Target Support Files/Pods-FabricExampleTestHost/Pods-FabricExampleTestHost.debug.xcconfig"; sourceTree = ""; }; - CC5FA2DEBE8EF281B0F41D00 /* Pods-FabricExampleTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FabricExampleTests.release.xcconfig"; path = "Target Support Files/Pods-FabricExampleTests/Pods-FabricExampleTests.release.xcconfig"; sourceTree = ""; }; - D41B1AB45DFBF5F12111BD53 /* Pods-FabricExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FabricExample.release.xcconfig"; path = "Target Support Files/Pods-FabricExample/Pods-FabricExample.release.xcconfig"; sourceTree = ""; }; - DC009E8601127E4B5DB8C238 /* libPods-FabricExampleTestHost.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-FabricExampleTestHost.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + D3CF8BD475787A710EE73FFF /* Pods-FabricExampleTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FabricExampleTests.release.xcconfig"; path = "Target Support Files/Pods-FabricExampleTests/Pods-FabricExampleTests.release.xcconfig"; sourceTree = ""; }; + DEC71FD978D91660598B9E7E /* Pods-FabricExampleTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FabricExampleTests.debug.xcconfig"; path = "Target Support Files/Pods-FabricExampleTests/Pods-FabricExampleTests.debug.xcconfig"; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; - EF53ABF265599E275947FD77 /* libPods-FabricExample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-FabricExample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -69,7 +69,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - F2EF6EA995C3D07711BB3E99 /* libPods-FabricExample.a in Frameworks */, + 6FCBD9016D95736D685FE800 /* libPods-FabricExample.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -77,7 +77,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 782077328032E706EB7A3E71 /* libPods-FabricExampleTests.a in Frameworks */, + D651FCF536FBFFFB277E52A4 /* libPods-FabricExampleTests.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -85,7 +85,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - C1BA078C772122BE5F5984EE /* libPods-FabricExampleTestHost.a in Frameworks */, + 4169E7BEA9F7E99D2BE7625F /* libPods-FabricExampleTestHost.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -108,9 +108,9 @@ isa = PBXGroup; children = ( ED297162215061F000B7C4FE /* JavaScriptCore.framework */, - EF53ABF265599E275947FD77 /* libPods-FabricExample.a */, - DC009E8601127E4B5DB8C238 /* libPods-FabricExampleTestHost.a */, - C7604CA8A8034C4311483555 /* libPods-FabricExampleTests.a */, + 1567B6CDDE90682EB57FEB33 /* libPods-FabricExample.a */, + 9C7A65B861FEEF104BF68E4A /* libPods-FabricExampleTestHost.a */, + 603B62CE61243E0661D1C698 /* libPods-FabricExampleTests.a */, ); name = Frameworks; sourceTree = ""; @@ -174,12 +174,12 @@ BBD78D7AC51CEA395F1C20DB /* Pods */ = { isa = PBXGroup; children = ( - 3C8DEB077790086AF6400A95 /* Pods-FabricExample.debug.xcconfig */, - D41B1AB45DFBF5F12111BD53 /* Pods-FabricExample.release.xcconfig */, - C7B9E83E1B54A0E19548A55D /* Pods-FabricExampleTestHost.debug.xcconfig */, - 75EABA3D2424D85D2A417C44 /* Pods-FabricExampleTestHost.release.xcconfig */, - A3B9705E574724C4530FA170 /* Pods-FabricExampleTests.debug.xcconfig */, - CC5FA2DEBE8EF281B0F41D00 /* Pods-FabricExampleTests.release.xcconfig */, + 4ED195C5A18E086D371F67C8 /* Pods-FabricExample.debug.xcconfig */, + 97F18A960BF8590E4768681F /* Pods-FabricExample.release.xcconfig */, + 56E271DBAE1677D0B8FEE0D8 /* Pods-FabricExampleTestHost.debug.xcconfig */, + 1C9B8045747B6E85492A2647 /* Pods-FabricExampleTestHost.release.xcconfig */, + DEC71FD978D91660598B9E7E /* Pods-FabricExampleTests.debug.xcconfig */, + D3CF8BD475787A710EE73FFF /* Pods-FabricExampleTests.release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -191,7 +191,7 @@ isa = PBXNativeTarget; buildConfigurationList = A1B2C3DF1E00000100A0A001 /* Build configuration list for PBXNativeTarget "FabricExampleTests" */; buildPhases = ( - 269A048D4251A0DBF5B87600 /* [CP] Check Pods Manifest.lock */, + 9BAF09056F41426B1070F709 /* [CP] Check Pods Manifest.lock */, A1B2C3DC1E00000100A0A001 /* Sources */, A1B2C3D71E00000100A0A001 /* Frameworks */, A1B2C3DB1E00000100A0A001 /* Resources */, @@ -210,13 +210,13 @@ isa = PBXNativeTarget; buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "FabricExample" */; buildPhases = ( - E5C516C31155AB52645ACBF5 /* [CP] Check Pods Manifest.lock */, + F6A627AA5F70C25C7E02E042 /* [CP] Check Pods Manifest.lock */, 13B07F871A680F5B00A75B9A /* Sources */, 13B07F8C1A680F5B00A75B9A /* Frameworks */, 13B07F8E1A680F5B00A75B9A /* Resources */, 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, - F8FE291572190367929C29B5 /* [CP] Embed Pods Frameworks */, - EB448967FE006C03D2449A2E /* [CP] Copy Pods Resources */, + 990967CA999D9F1563B41777 /* [CP] Embed Pods Frameworks */, + E4B2FA8EF31623DE1B2E33E1 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -231,12 +231,12 @@ isa = PBXNativeTarget; buildConfigurationList = B2C3D5D1E00000100A0A001 /* Build configuration list for PBXNativeTarget "FabricExampleTestHost" */; buildPhases = ( - DDDB6CC2B2FAC22714592681 /* [CP] Check Pods Manifest.lock */, + 56DFEAA8649728DD47615703 /* [CP] Check Pods Manifest.lock */, B2C3D571E00000100A0A001 /* Sources */, B2C3D581E00000100A0A001 /* Frameworks */, B2C3D591E00000100A0A001 /* Resources */, - DDE3D22E77D4C7D806FFDC3C /* [CP] Embed Pods Frameworks */, - 36B4BEE30DAC02B894B79786 /* [CP] Copy Pods Resources */, + 208C11CC11F26797809F91C7 /* [CP] Embed Pods Frameworks */, + 9E3583B9F877362D206032AB /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -328,46 +328,24 @@ shellPath = /bin/sh; shellScript = "set -e\n\nWITH_ENVIRONMENT=\"$REACT_NATIVE_PATH/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"$REACT_NATIVE_PATH/scripts/react-native-xcode.sh\"\n\n/bin/sh -c \"$WITH_ENVIRONMENT $REACT_NATIVE_XCODE\"\n"; }; - 269A048D4251A0DBF5B87600 /* [CP] Check Pods Manifest.lock */ = { + 208C11CC11F26797809F91C7 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-FabricExampleTestHost/Pods-FabricExampleTestHost-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-FabricExampleTests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - 36B4BEE30DAC02B894B79786 /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-FabricExampleTestHost/Pods-FabricExampleTestHost-resources-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Copy Pods Resources"; + name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-FabricExampleTestHost/Pods-FabricExampleTestHost-resources-${CONFIGURATION}-output-files.xcfilelist", + "${PODS_ROOT}/Target Support Files/Pods-FabricExampleTestHost/Pods-FabricExampleTestHost-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-FabricExampleTestHost/Pods-FabricExampleTestHost-resources.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-FabricExampleTestHost/Pods-FabricExampleTestHost-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - DDDB6CC2B2FAC22714592681 /* [CP] Check Pods Manifest.lock */ = { + 56DFEAA8649728DD47615703 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -389,24 +367,24 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - DDE3D22E77D4C7D806FFDC3C /* [CP] Embed Pods Frameworks */ = { + 990967CA999D9F1563B41777 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-FabricExampleTestHost/Pods-FabricExampleTestHost-frameworks-${CONFIGURATION}-input-files.xcfilelist", + "${PODS_ROOT}/Target Support Files/Pods-FabricExample/Pods-FabricExample-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-FabricExampleTestHost/Pods-FabricExampleTestHost-frameworks-${CONFIGURATION}-output-files.xcfilelist", + "${PODS_ROOT}/Target Support Files/Pods-FabricExample/Pods-FabricExample-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-FabricExampleTestHost/Pods-FabricExampleTestHost-frameworks.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-FabricExample/Pods-FabricExample-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - E5C516C31155AB52645ACBF5 /* [CP] Check Pods Manifest.lock */ = { + 9BAF09056F41426B1070F709 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -421,14 +399,31 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-FabricExample-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-FabricExampleTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - EB448967FE006C03D2449A2E /* [CP] Copy Pods Resources */ = { + 9E3583B9F877362D206032AB /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-FabricExampleTestHost/Pods-FabricExampleTestHost-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-FabricExampleTestHost/Pods-FabricExampleTestHost-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-FabricExampleTestHost/Pods-FabricExampleTestHost-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + E4B2FA8EF31623DE1B2E33E1 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -445,21 +440,26 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-FabricExample/Pods-FabricExample-resources.sh\"\n"; showEnvVarsInLog = 0; }; - F8FE291572190367929C29B5 /* [CP] Embed Pods Frameworks */ = { + F6A627AA5F70C25C7E02E042 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-FabricExample/Pods-FabricExample-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - name = "[CP] Embed Pods Frameworks"; + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-FabricExample/Pods-FabricExample-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-FabricExample-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-FabricExample/Pods-FabricExample-frameworks.sh\"\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -508,7 +508,7 @@ /* Begin XCBuildConfiguration section */ 13B07F941A680F5B00A75B9A /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 3C8DEB077790086AF6400A95 /* Pods-FabricExample.debug.xcconfig */; + baseConfigurationReference = 4ED195C5A18E086D371F67C8 /* Pods-FabricExample.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; @@ -548,7 +548,7 @@ }; 13B07F951A680F5B00A75B9A /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = D41B1AB45DFBF5F12111BD53 /* Pods-FabricExample.release.xcconfig */; + baseConfigurationReference = 97F18A960BF8590E4768681F /* Pods-FabricExample.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; @@ -778,7 +778,7 @@ }; A1B2C3DD1E00000100A0A001 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = A3B9705E574724C4530FA170 /* Pods-FabricExampleTests.debug.xcconfig */; + baseConfigurationReference = DEC71FD978D91660598B9E7E /* Pods-FabricExampleTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ENABLE_MODULES = YES; @@ -806,7 +806,7 @@ }; A1B2C3DE1E00000100A0A001 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = CC5FA2DEBE8EF281B0F41D00 /* Pods-FabricExampleTests.release.xcconfig */; + baseConfigurationReference = D3CF8BD475787A710EE73FFF /* Pods-FabricExampleTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ENABLE_MODULES = YES; @@ -835,7 +835,7 @@ }; B2C3D5E1E00000100A0A001 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = C7B9E83E1B54A0E19548A55D /* Pods-FabricExampleTestHost.debug.xcconfig */; + baseConfigurationReference = 56E271DBAE1677D0B8FEE0D8 /* Pods-FabricExampleTestHost.debug.xcconfig */; buildSettings = { CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = 1; @@ -874,7 +874,7 @@ }; B2C3D5F1E00000100A0A001 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 75EABA3D2424D85D2A417C44 /* Pods-FabricExampleTestHost.release.xcconfig */; + baseConfigurationReference = 1C9B8045747B6E85492A2647 /* Pods-FabricExampleTestHost.release.xcconfig */; buildSettings = { CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = 1; diff --git a/package.json b/package.json index 81314d087..38860acab 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "clean": "del-cli packages/**/android/build apps/**/android/build apps/**/android/app/build apps/**/ios/build packages/**/lib node_modules apps/**/node_modules packages/**/node_modules", "typecheck": "yarn workspaces foreach -A -p run typecheck", "test": "yarn workspace react-native-audio-api run test", - "check-audio-enum-sync": "bash packages/react-native-audio-api/scripts/check-audio-events-sync.sh", + "check-audio-enum-sync": "bash packages/react-native-audio-api/scripts/check-enum-sync.sh", "validate:fast": "bash scripts/validate.sh --fast", "validate:cpp": "bash scripts/validate.sh --cpp", "validate:cpp-extended": "bash scripts/validate.sh --cpp-extended", diff --git a/packages/audiodocs/docs/inputs/audio-recorder.mdx b/packages/audiodocs/docs/inputs/audio-recorder.mdx index 1c7c5b8b2..21aa1da0a 100644 --- a/packages/audiodocs/docs/inputs/audio-recorder.mdx +++ b/packages/audiodocs/docs/inputs/audio-recorder.mdx @@ -384,16 +384,10 @@ const audioRecorder = new AudioRecorder({ Starts the stream from the system audio input device. -| Parameter | Type | Description | -| :---: | :---: | :---- | -| `options` | [`AudioRecorderStartOptions`](#audiorecorderstartoptions) | Optional recording start configuration. | - ###### Returns `Promise>`. ```tsx -const result = await audioRecorder.start({ - fileNameOverride: `my_audio_${mySessionId}`, -}); +const result = await audioRecorder.start(); console.log(result.status); ``` @@ -607,18 +601,6 @@ type AndroidInputPreset = Names of Oboe's [`InputPreset`](https://github.com/google/oboe/blob/0da326e4ef878eac0c032e11ea84ca0a6811aafd/include/oboe/Definitions.h#L470) values, which select the preprocessing chain the capture stream is opened with. -#### `AudioRecorderStartOptions` - -```tsx -interface AudioRecorderStartOptions { - fileNameOverride?: string; -} -``` - -| Parameter | Type | Description | -| :---: | :---: | :---- | -| `fileNameOverride` | `string` | Custom file name used when recording to file. | - #### AudioRecorderCallbackOptions ```tsx @@ -675,13 +657,13 @@ interface AudioRecorderFileOptions { directory?: FileDirectory; subDirectory?: string; - fileNamePrefix?: string; + fileName?: string; androidFlushIntervalMs?: number; } ``` - `channelCount` - The desired channel count in the resulting file. not all file formats supports all possible channel counts. -- `rotateIntervalBytes` - The threshold size (in bytes) at which the recorder will start writing to a new file. If set to `0` (default), file output rotation is disabled. When active, new files are named with the original prefix appended with a timestamp. You can join the rotated files after recording with [`concatAudioFiles`](../utils/file-concatenation.mdx#concataudiofiles). +- `rotateIntervalBytes` - The threshold size (in bytes) at which the recorder will start writing to a new file. If set to `0` (default), file output rotation is disabled. When active, each segment carries a three-digit index (`_001`, `_002`, …) — see [File naming](#file-naming). You can join the rotated files after recording with [`concatAudioFiles`](../utils/file-concatenation.mdx#concataudiofiles). - Use a large enough value for your format. Very small thresholds rotate often, which increases the chance of audible gaps or muffled joins after concatenation — especially for **M4A**, where each segment is a separate AAC encode. - Practical starting points: **≥ 1 MB** for WAV, **≥ 200 KB** for M4A (adjust upward if you still hear artifacts at segment boundaries). - This option controls segment file size, not RAM usage. For crash-resilience tuning on Android, use `androidFlushIntervalMs` instead. @@ -689,26 +671,87 @@ interface AudioRecorderFileOptions { - `preset` - The desired recorder file properties, you can use either one of built-in properties or tweak low-level parameters yourself. Check [FilePresetType](#filepresettype) for more details. - `directory` - Either `FileDirectory.Cache` or `FileDirectory.Document` (default: `FileDirectory.Cache`). Determines the system directory that the file will be saved to. - `subDirectory` - If configured it will create the recording inside requested directory (default: `undefined`). -- `fileNamePrefix` - Prefix of the recording files without the unique ID (default: `recording`). +- `fileName` - Names the output file outright. Left unset, the library generates `recording_`. See [File naming](#file-naming) below. - `androidFlushIntervalMs` - How often the recorder should force the system to write data to the device storage (default: `500`). - Lower values are good for crash-resilience and are more memory friendly. - Higher values are more battery - and storage-efficient. +#### File naming + +`fileName` names the output file outright. Left unset, the library generates a name that is +unique by construction. Rotation appends a segment index either way, because one recording +then spans several files. + +| `fileName` | `rotateIntervalBytes` | Resulting file(s) | +| :--- | :--- | :--- | +| — | `0` | `recording_20260909_101500.wav` | +| `session-42` | `0` | `session-42.wav` | +| — | `> 0` | `recording_20260909_101500_001.wav`, `_002`, … | +| `session-42` | `> 0` | `session-42_001.wav`, `session-42_002.wav`, … | + +The generated timestamp is taken once per recording, in local time, so every segment of one +rotated recording shares it and only the index differs. + +`fileName` has to be a bare name: no extension (it follows from `format`), no path +separators and no `..`. + +The segment index is padded to three digits and keeps counting beyond that (`_999`, +`_1000`, …). Names stay unique; only their lexicographic order breaks past 999 segments — +`stop()` still returns `paths` in recording order. + +A recording without rotation can still span several files: on iOS, an audio route change +mid-session (headphones connecting, say) alters the input format, and the encoder has to be +reopened. The first file keeps its plain name and each reopened file takes the next index +(`session-42.wav`, then `session-42_001.wav`). `stop()` returns every file, and its size and +duration cover all of them. + +:::caution +`fileName` puts you in charge of telling recordings apart. An existing file of the same name +is **overwritten**, with a warning in the native log, and two recordings started under the +same `fileName` overwrite each other — with rotation, segment by segment, since numbering +restarts at `_001` every time. Make the name unique per recording yourself. +::: + #### FileFormat Describes desired file extension as well as codecs, containers (and muxers!) used to encode the file. +All encoding is done with platform system APIs — iOS AVFoundation and Android MediaCodec/MediaMuxer. Because each platform exposes a different set of system encoders, format support is platform-specific. + ```tsx enum FileFormat { Wav, Caf, M4A, Flac, + Aiff, + Alac, + OpusOgg, + OpusWebm, + VorbisWebm, + Ulaw, + Alaw, } ``` -:::caution Android + FFmpeg -On Android, encoded file output for `M4A`, `FLAC`, and `CAF` uses FFmpeg. When FFmpeg is disabled in the build, only **WAV** recording to file is supported. iOS uses system AVFoundation for all listed formats. See [Runtime flags](../other/runtime-flags.mdx#where-ffmpeg-is-used). +The table below lists which formats each platform can encode with its system APIs: + +| `FileFormat` | Container / codec | iOS | Android | +| :--- | :--- | :---: | :---: | +| `Wav` | WAV / PCM | ✅ | ✅ | +| `M4A` | M4A / AAC-LC | ✅ | ✅ | +| `Flac` | FLAC / FLAC | ✅ | ✅ | +| `Caf` | CAF / PCM | ✅ | ❌ | +| `Aiff` | AIFF / PCM | ✅ | ❌ | +| `Alac` | M4A / Apple Lossless | ✅ | ❌ | +| `Ulaw` | WAV / µ-law | ✅ | ❌ | +| `Alaw` | WAV / a-law | ✅ | ❌ | +| `OpusOgg` | OGG / Opus | ❌ | ✅ | +| `OpusWebm` | WebM / Opus | ❌ | ✅ | +| `VorbisWebm` | WebM / Vorbis | ❌ | ✅ | + +:::caution Platform support +Selecting a format the current platform cannot encode (for example `Caf` on Android or `OpusOgg` on iOS) fails when file output is enabled, with a descriptive error. Some Android formats also depend on the device OS version (Opus/OGG muxing requires newer releases); if a device lacks a system encoder for the requested format, recording start returns an error. Use `Wav`, `M4A`, or `Flac` for the widest cross-platform support. ::: #### FileInfo diff --git a/packages/audiodocs/docs/other/disabling-prebuilt-libraries.mdx b/packages/audiodocs/docs/other/disabling-prebuilt-libraries.mdx index 6cd9faa22..39d986252 100644 --- a/packages/audiodocs/docs/other/disabling-prebuilt-libraries.mdx +++ b/packages/audiodocs/docs/other/disabling-prebuilt-libraries.mdx @@ -17,7 +17,7 @@ The available flags are independent and can be combined: | Flag | What it removes | What stops working | | :---: | :---- | :---- | -| `disableFFmpeg` | FFmpeg shared libraries (`libavcodec`, `libavformat`, `libavutil`, `libswresample`) | Remote URL streaming / HLS, remote URL metadata, M4A concat, **Android** non-WAV recording — see [Runtime flags](./runtime-flags.mdx#where-ffmpeg-is-used) | +| `disableFFmpeg` | FFmpeg shared libraries (`libavcodec`, `libavformat`, `libavutil`, `libswresample`) | Remote URL streaming / HLS, remote URL metadata — see [Runtime flags](./runtime-flags#where-ffmpeg-is-used). | | `disableStaticExternalLibs` | Static libs: `libopus`, `libopusfile`, `libogg`, `libvorbis`, `libvorbisenc`, `libvorbisfile` | Decoding `ogg`, `opus`, `oga` files | :::info diff --git a/packages/audiodocs/docs/other/runtime-flags.mdx b/packages/audiodocs/docs/other/runtime-flags.mdx index c65709891..6aa079f5b 100644 --- a/packages/audiodocs/docs/other/runtime-flags.mdx +++ b/packages/audiodocs/docs/other/runtime-flags.mdx @@ -2,7 +2,7 @@ These helpers let you check at runtime which optional native features are compiled into your app. They are synchronous and safe to call from JavaScript after the library has been installed. -Use them to branch UI or loading logic — for example, skip remote metadata preload when FFmpeg is disabled, disable hls streaming, or offer only WAV recording on Android. +Use them to branch UI or loading logic — for example, skip remote metadata preload when FFmpeg is disabled or disable hls streaming. :::info Build-time vs runtime To **disable** optional libraries at build time (and reduce app size), see [Disabling prebuilt libraries](./disabling-prebuilt-libraries.mdx). Runtime flags only tell you what ended up in the binary you are running. @@ -20,7 +20,7 @@ Returns whether the native build includes [`FFmpeg`](https://github.com/FFmpeg/F import { isFfmpegEnabled } from 'react-native-audio-api'; if (!isFfmpegEnabled()) { - console.warn('Remote URL metadata, streaming, and Android M4A recording require an FFmpeg build.'); + console.warn('Remote URL metadata and streaming require an FFmpeg build.'); } ``` @@ -29,14 +29,12 @@ if (!isFfmpegEnabled()) { | Area | API | Requires FFmpeg for | | :---: | :---: | :---- | -| Streaming | [`Audio tag`](../sources/audio-tag.mdx), `createFileSource` | Remote URL streaming (HTTP byte ranges) and HLS (`.m3u8`) | -| Metadata | [`getAudioDuration`](../utils/decoding.mdx#getaudioduration), [`