Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude/skills/post-work-checks/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,13 +119,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).

---

Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/post-work-checks/maintenance.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` / `--graph` / `--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`, `graph-tests.yml` | What CI covers vs local validation tiers |
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,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:
Expand Down
15 changes: 13 additions & 2 deletions apps/common-app/src/demos/Record/Record.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ import RecordingVisualization from './RecordingVisualization';
import Status from './Status';
import { RecordingState } from './types';

const RECORDING_EXTENSION = FileFormat.Wav;

const RECORDING_EXTENSION_NAME_MAP = {
[FileFormat.Wav]: 'wav',
[FileFormat.M4A]: 'm4a',
}
const Record: FC = () => {
const [state, setState] = useState<RecordingState>(RecordingState.Idle);
const [hasPermissions, setHasPermissions] = useState<boolean>(false);
Expand Down Expand Up @@ -130,8 +136,13 @@ 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}`
);

console.log(info.paths.length);
const finalPath = await concatAudioFiles(info.paths, outputPath);
const audioBuffer = await audioContext.decodeAudioData(finalPath);
setRecordedBuffer(audioBuffer);
Expand Down Expand Up @@ -262,7 +273,7 @@ const Record: FC = () => {
}, [onPauseRecording, onResumeRecording]);

useEffect(() => {
Recorder.enableFileOutput({ rotateIntervalBytes: 1_000_000, format: FileFormat.M4A });
Recorder.enableFileOutput({ rotateIntervalBytes: 1_000_000, format: RECORDING_EXTENSION });

return () => {
stopPlayback();
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,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:graph": "bash scripts/validate.sh --graph",
"validate:android": "bash scripts/validate.sh --android",
Expand Down
41 changes: 39 additions & 2 deletions packages/audiodocs/docs/inputs/audio-recorder.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -654,17 +654,54 @@ interface AudioRecorderFileOptions {

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. FFmpeg is never used for encoding. 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,
AmrNb,
AmrWb,
AacHe,
AacEld,
Ima4,
Ulaw,
Alaw,
Ilbc,
}
```

:::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](/docs/other/runtime-flags#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 | ✅ | ❌ |
| `AacHe` | M4A / AAC-HE | ✅ | ❌ |
| `AacEld` | M4A / AAC-ELD | ✅ | ❌ |
| `Ima4` | CAF / IMA4 (ADPCM) | ✅ | ❌ |
| `Ulaw` | WAV / µ-law | ✅ | ❌ |
| `Alaw` | WAV / a-law | ✅ | ❌ |
| `Ilbc` | CAF / iLBC | ✅ | ❌ |
| `OpusOgg` | OGG / Opus | ❌ | ✅ |
| `OpusWebm` | WebM / Opus | ❌ | ✅ |
| `VorbisWebm` | WebM / Vorbis | ❌ | ✅ |
| `AmrNb` | 3GP / AMR-NB | ❌ | ✅ |
| `AmrWb` | 3GP / AMR-WB | ❌ | ✅ |

:::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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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`) | Streaming, remote playback/metadata, `.aac` / `.mp4` / `.m4a` decode, M4A concat, **Android** non-WAV recording — see [Runtime flags](/docs/other/runtime-flags#where-ffmpeg-is-used) |
| `disableFFmpeg` | FFmpeg shared libraries (`libavcodec`, `libavformat`, `libavutil`, `libswresample`) | Streaming, remote playback/metadata, `.aac` / `.mp4` / `.m4a` decode — see [Runtime flags](/docs/other/runtime-flags#where-ffmpeg-is-used). Recording and `concatAudioFiles` are unaffected. |
| `disableStaticExternalLibs` | Static libs: `libopus`, `libopusfile`, `libogg`, `libvorbis`, `libvorbisenc`, `libvorbisfile` | Decoding `ogg`, `opus`, `oga` files |

:::info
Expand Down
17 changes: 12 additions & 5 deletions packages/audiodocs/docs/other/runtime-flags.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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](/docs/other/disabling-prebuilt-libraries). Runtime flags only tell you what ended up in the binary you are running.
Expand All @@ -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.');
}
```
</details>
Expand All @@ -33,12 +33,19 @@ if (!isFfmpegEnabled()) {
| Playback | `createFileSource`, [`<Audio>`](/docs/sources/audio-tag) | Remote URL streaming, HLS (`.m3u8`), local `.mp4` / `.m4a` / `.aac`. Without FFmpeg, remote sources are downloaded and decoded with miniaudio where supported. |
| Decoding | [`decodeAudioData`](/docs/utils/decoding#decodeaudiodata) | `.aac`, `.mp4`, `.m4a` (and remote URLs for any format) |
| Metadata | [`getAudioDuration`](/docs/utils/decoding#getaudioduration), [`<Audio>`](/docs/sources/audio-tag) `preload="metadata"` | Remote `http(s):` URL duration probing |
| Recording | [`AudioRecorder`](/docs/inputs/audio-recorder) file output | **Android only:** `M4A`, `FLAC`, and `CAF` (WAV uses miniaudio). iOS uses system AVFoundation instead. |
| File utils | [`concatAudioFiles`](/docs/utils/file-concatenation#concataudiofiles) | M4A concatenation (WAV uses miniaudio) |

Formats such as `.mp3`, `.wav`, and `.flac` are often handled by miniaudio for **local** decode paths and for **downloaded** remote sources when FFmpeg is disabled.

When `isFfmpegEnabled()` returns `false`, the features in the table above are unavailable or fall back to errors. On **Android**, [`AudioRecorder`](/docs/inputs/audio-recorder) file output is limited to **WAV**.
:::note Recording and file concatenation are FFmpeg-free
[`AudioRecorder`](/docs/inputs/audio-recorder) file output and
[`concatAudioFiles`](/docs/utils/file-concatenation#concataudiofiles) do **not**
use FFmpeg on any platform. Recording encodes with system APIs (iOS AVFoundation,
Android MediaCodec/MediaMuxer). `concatAudioFiles` supports **WAV** (miniaudio) and
**M4A/MP4 AAC** (system remux). Neither is affected by `isFfmpegEnabled()`.
See the [per-platform format matrix](/docs/inputs/audio-recorder#fileformat).
:::

When `isFfmpegEnabled()` returns `false`, the features in the table above are unavailable or fall back to errors.

### Disabling FFmpeg

Expand Down
14 changes: 10 additions & 4 deletions packages/audiodocs/docs/utils/file-concatenation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,20 @@ recording is finished.
:::

:::caution
**M4A and WAV concatenation is only supported.**
**WAV and M4A concatenation is only supported.**

M4A format requires FFmpeg to be available in the native build. WAV
concatenation uses miniaudio and does not require FFmpeg. See [Runtime flags](/docs/other/runtime-flags#where-ffmpeg-is-used) for details.
WAV uses miniaudio (PCM remux). M4A/MP4 remuxes AAC packets with system APIs —
iOS AVFoundation and Android MediaExtractor/MediaMuxer — and does **not**
require FFmpeg.
:::

:::tip Segment size and quality
`concatAudioFiles` joins segments losslessly for **WAV** (PCM re-mux). For **M4A**, it remuxes encoded AAC packets from separately encoded segments. Each rotated chunk starts a fresh AAC encoder, so very small [`rotateIntervalBytes`](/docs/inputs/audio-recorder#audiorecorderfileoptions) values can produce clicks, muffled joins, or timing gaps after concatenation.
`concatAudioFiles` joins segments losslessly for **WAV** (PCM remux). For **M4A**,
Android remuxes the encoded AAC packets losslessly, while iOS re-encodes the
joined audio once through AVFoundation. Each rotated chunk is
a separate AAC encode, so very small
[`rotateIntervalBytes`](/docs/inputs/audio-recorder#audiorecorderfileoptions)
values can still produce clicks, muffled joins, or timing gaps after concatenation.

If playback quality is poor after joining rotated M4A files, increase `rotateIntervalBytes` (for example **≥ 200 KB**) or record without rotation. WAV segments are less sensitive, but the same guidance applies below roughly **512 KB–1 MB** per segment.
:::
Expand Down
Loading
Loading