Skip to content
Open
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
1 change: 1 addition & 0 deletions .claude/skills/build-compilation-dependencies/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,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` |
| Android incremental build only: `Unresolved reference` to a top-level Kotlin function, or `Cannot access '…': it is internal in file`, while `--rerun-tasks` passes | Incremental Kotlin rounds treat the module's own earlier classes as a foreign module (library is built through the `node_modules/react-native-audio-api` symlink) | Use only public classes/objects across files in `android/src/main/java`; no top-level functions, no `internal` |
| clangd only: `'React/RCTBridgeModule.h' file not found` in `.mm` files | `compile_commands.json` has no framework search path | See *clangd compile database* below — regenerate with `yarn setup:clangd` |

## clangd compile database
Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/thread-safety-itc/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ Per-quantum processable state (`ALWAYS_`/`CONDITIONAL_`/`NOT_PROCESSABLE`) is de
| Non-primitive, can be written by audio thread | Triple buffer (see `AnalyserNode` for reference) |
| CPU-heavy work, must not block JS or audio | `TaskOffloader` on a dedicated worker thread |
| Context lifecycle (`resume`/`suspend`/`close`) | `scheduleContextPromise` → `pendingPromisesOffloader_` |
| Platform code must reach the live recorder without going through JS | Process-global handle (`ActiveRecorderHandle` — recursive mutex + `weak_ptr`; `tryStart` / `stopAndReturnInfo` / `stopAndReturnState` / `pause` / `resume` take the handle mutex before `AudioRecorder`). Android: static-JNI `NativeRecorderControl` (no HybridData). iOS: `AudioAPIModule.setAudioSessionActivity(false)` calls `stopAndReturnInfo()` before deactivating the session. HostObject `start` / `stop` go through the handle; HostObject `pause` / `resume` still call `AudioRecorder` directly. Android blocking calls run on a Kotlin executor (`goAsync()` in receivers), never a detached `std::thread` |
| Platform code must reach the live recorder without going through JS | Process-global handle (`ActiveRecorderHandle` — recursive mutex + `weak_ptr`; `tryStart` / `stopAndReturnInfo` / `stopAndReturnState` / `pause` / `resume` take the handle mutex before `AudioRecorder`). Android: static-JNI `NativeRecorderControl` (no HybridData). iOS: `AudioAPIModule.setSystemActivity(false)` calls `stopAndReturnInfo()` before deactivating the session. HostObject `start` / `stop` go through the handle; HostObject `pause` / `resume` still call `AudioRecorder` directly. Android blocking calls run on a Kotlin executor (`goAsync()` in receivers), never a detached `std::thread` |

---

Expand Down
4 changes: 2 additions & 2 deletions .claude/skills/turbo-modules/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ Defines the TypeScript interface for the native module. Codegen (React Native's
interface Spec extends TurboModule {
install(): boolean; // synchronous — MUST run first
getDevicePreferredSampleRate(): number; // synchronous
setAudioSessionActivity(enabled: boolean): Promise<boolean>;
setAudioSessionOptions(...): void;
setSystemActivity(enabled: boolean): Promise<boolean>;
setSystemOptions(...): void;
observeAudioInterruptions(focusType, enabled): void;
requestRecordingPermissions(): Promise<PermissionStatus>;
// ... audio devices, notifications ...
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ packages/custom-node-generator/ # Code generation tooling
- **Optional FFmpeg**: Audio decoding via FFmpeg can be conditionally compiled out
- **Audio Worklets**: JavaScript runs on the audio thread via React Native Worklets
- **Notification-Driven Foreground Service (Android)**: `NotificationRegistry.showNotification` → `ForegroundServiceManager.subscribe` → `CentralizedForegroundService`; service lifetime follows notification visibility, never recorder/player state. The library manifest is empty — consuming apps declare the `<service>` (Expo plugin `withAudioAPI.ts` or manually), where `android:stopWithTask` (plugin option `androidFSStopWithTask`) decides whether the service and an in-progress recording survive task removal. The recording notification's swipe policy (`dismissible` option) is fixed when the notification is built and travels as an extra of its delete intent, so `RecordingNotificationReceiver` stays stateless: a pinned notification is re-posted after a swipe (Android 14+ lets users swipe `setOngoing(true)` notifications; 13 honours the flag; 12 and older pin every foreground-service notification), a dismissible one stops the recording through the same path as the stop action
- **JS-Independent Recorder Control**: `ActiveRecorderHandle` (common C++, one-slot `weak_ptr` published by `tryStart`, released by a successful stop) is the process-global occupant. Android notification actions reach it through static-JNI `NativeRecorderControl` (no HybridData/React context — the reverse of `NativeFileInfo`). iOS `setAudioSessionActivity(false)` stops a live recording through the same handle before deactivating the session. Session methods take the handle mutex before `AudioRecorder`. `pause` / `resume` / `stopAndReturnState` / `currentState` return `RecorderState` (`core/inputs/RecorderState.h`, Kotlin ordinal mirror) so `RecordingNotificationReceiver` can render from that value. `stopAndReturnInfo` returns the recorder's file-info `Result` (JS `stop`). Successful stops with paths stash consume-once for `AudioRecorder.consumeLastRecordingResult()`; `AudioRecorder.isRecordingOngoing()` probes a session that outlived the UI
- **JS-Independent Recorder Control**: `ActiveRecorderHandle` (common C++, one-slot `weak_ptr` published by `tryStart`, released by a successful stop) is the process-global occupant. Android notification actions reach it through static-JNI `NativeRecorderControl` (no HybridData/React context — the reverse of `NativeFileInfo`). iOS `setSystemActivity(false)` stops a live recording through the same handle before deactivating the session. Session methods take the handle mutex before `AudioRecorder`. `pause` / `resume` / `stopAndReturnState` / `currentState` return `RecorderState` (`core/inputs/RecorderState.h`, Kotlin ordinal mirror) so `RecordingNotificationReceiver` can render from that value. `stopAndReturnInfo` returns the recorder's file-info `Result` (JS `stop`). Successful stops with paths stash consume-once for `AudioRecorder.consumeLastRecordingResult()`; `AudioRecorder.isRecordingOngoing()` probes a session that outlived the UI
- **Testable C++ dependencies**: consumers take interface types (`std::shared_ptr<I…>`); construct concrete implementations only at platform bootstrap. Example: audio event registry (use `IAudioEventHandlerRegistry` more often than `AudioEventHandlerRegistry`).

### Native Module Entry Points
Expand Down
6 changes: 3 additions & 3 deletions apps/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ apps/
Bottom Tabs
├── Tests → 2-column grid of Examples (simple feature demos)
├── Demo Apps → Single-column list of Demos (full mini-apps)
└── Other → Placeholder
└── Other → Internal tooling / repro screens, registered in `other/index.ts` (`otherScreens`); add the key to `NavigationParamList` in `examples/index.ts`
```

Each item navigates into a Stack screen. No manual stack registration needed — `examples/index.ts` and `demos/index.ts` drive it automatically.
Expand Down Expand Up @@ -264,12 +264,12 @@ function MyPedal({ context, inputNode, outputNode }: PedalProps) {
```tsx
import { AudioManager } from 'react-native-audio-api';

AudioManager.setAudioSessionOptions({
AudioManager.setSystemOptions({
iosCategory: 'playback', // or 'playAndRecord' for mic input
iosMode: 'default',
iosOptions: [],
});
await AudioManager.setAudioSessionActivity(true);
await AudioManager.setSystemActivity(true);
```

### Recording permission
Expand Down
6 changes: 3 additions & 3 deletions apps/common-app/src/demos/Crossfade/Crossfade.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -160,12 +160,12 @@ const Crossfade: FC = () => {
return;
}

AudioManager.setAudioSessionOptions({
AudioManager.setSystemOptions({
iosCategory: 'playback',
iosMode: 'default',
iosOptions: [],
});
await AudioManager.setAudioSessionActivity(true);
await AudioManager.setSystemActivity(true);

if (audioContext.current.state === 'suspended') {
await audioContext.current.resume();
Expand Down Expand Up @@ -226,7 +226,7 @@ const Crossfade: FC = () => {
gainNode2.current = null;

await audioContext.current.suspend();
await AudioManager.setAudioSessionActivity(false);
await AudioManager.setSystemActivity(false);
setIsPlaying(false);
}, [isPlaying]);

Expand Down
4 changes: 2 additions & 2 deletions apps/common-app/src/demos/PedalBoard/PedalBoard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ export default function PedalBoard() {
useEffect(() => {
const init = async () => {
setIsLoading(true);
AudioManager.setAudioSessionOptions({
AudioManager.setSystemOptions({
iosCategory: 'playAndRecord',
iosMode: 'default',
})
AudioManager.setAudioSessionActivity(true);
AudioManager.setSystemActivity(true);

try {
// Load audio file
Expand Down
6 changes: 3 additions & 3 deletions apps/common-app/src/demos/Record/Record.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,14 @@ const Record: FC = () => {
setHasPermissions(true);
}

AudioManager.setAudioSessionOptions({
AudioManager.setSystemOptions({
iosCategory: 'playAndRecord',
iosMode: 'default',
iosOptions: ['defaultToSpeaker', 'allowBluetoothA2DP'],
});

try {
await AudioManager.setAudioSessionActivity(true);
await AudioManager.setSystemActivity(true);
} catch (error) {
console.error(error);
Alert.alert('Error', 'Failed to activate audio session for recording.');
Expand Down Expand Up @@ -337,7 +337,7 @@ const Record: FC = () => {
stopPlayback();

if (!AudioRecorder.isRecordingOngoing()) {
AudioManager.setAudioSessionActivity(false);
AudioManager.setSystemActivity(false);
}
};
}, [stopPlayback]);
Expand Down
8 changes: 4 additions & 4 deletions apps/common-app/src/examples/AudioFile/AudioFile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,14 @@ const AudioFile: FC = () => {
setPositionPercentage(offset);
});

AudioManager.setAudioSessionOptions({
AudioManager.setSystemOptions({
iosCategory: 'playback',
iosMode: 'default',
iosOptions: [],
});

try {
await AudioManager.setAudioSessionActivity(true);
await AudioManager.setSystemActivity(true);
} catch (error) {
console.error('Failed to activate audio session:', error);
Alert.alert(
Expand Down Expand Up @@ -100,7 +100,7 @@ const AudioFile: FC = () => {
return () => {
AudioPlayer.reset();
PlaybackNotificationManager.hide();
AudioManager.setAudioSessionActivity(false);
AudioManager.setSystemActivity(false);
};
}, [fetchAudioBuffer]);

Expand Down Expand Up @@ -152,7 +152,7 @@ const AudioFile: FC = () => {
if (event.type === 'ended' && wasPlaying) {
BackgroundTimer.setTimeout(async () => {
AudioPlayer.setVolume(1.0);
AudioManager.setAudioSessionActivity(true);
AudioManager.setSystemActivity(true);
await AudioPlayer.play();
setIsPlaying(true);
setWasPlaying(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,12 @@ const ChannelMergerSplitter: FC = () => {
return;
}

AudioManager.setAudioSessionOptions({
AudioManager.setSystemOptions({
iosCategory: 'playback',
iosMode: 'default',
iosOptions: [],
});
await AudioManager.setAudioSessionActivity(true);
await AudioManager.setSystemActivity(true);

if (ctx.state === 'suspended') {
await ctx.resume();
Expand Down Expand Up @@ -170,7 +170,7 @@ const ChannelMergerSplitter: FC = () => {
bufferRef.current = null;
ctx.close().catch(() => {});
audioContextRef.current = null;
AudioManager.setAudioSessionActivity(false).catch(() => {});
AudioManager.setSystemActivity(false).catch(() => {});
};
}, [teardown]);

Expand Down
8 changes: 4 additions & 4 deletions apps/common-app/src/examples/ConvolverIR/ConvolverIR.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ const ConvolverIR: FC = () => {
bufferRef.current = null;
ctx.close().catch(() => {});
audioContextRef.current = null;
AudioManager.setAudioSessionActivity(false).catch(() => {});
AudioManager.setSystemActivity(false).catch(() => {});
};
}, []);

Expand All @@ -72,7 +72,7 @@ const ConvolverIR: FC = () => {
return;
}
bufferSourceRef.current?.stop(0);
// await AudioManager.setAudioSessionActivity(false);
// await AudioManager.setSystemActivity(false);
setIsPlaying(false);
}, []);

Expand All @@ -88,12 +88,12 @@ const ConvolverIR: FC = () => {
return;
}

AudioManager.setAudioSessionOptions({
AudioManager.setSystemOptions({
iosCategory: 'playback',
iosMode: 'default',
iosOptions: [],
});
await AudioManager.setAudioSessionActivity(true);
await AudioManager.setSystemActivity(true);

if (ctx.state === 'suspended') {
await ctx.resume();
Expand Down
Loading
Loading