Skip to content

Mobile: Fix #14804: Migrate expo-av to expo-audio#14847

Open
gherardi wants to merge 11 commits intolaurent22:devfrom
gherardi:migrate-expo-av-to-expo-audio
Open

Mobile: Fix #14804: Migrate expo-av to expo-audio#14847
gherardi wants to merge 11 commits intolaurent22:devfrom
gherardi:migrate-expo-av-to-expo-audio

Conversation

@gherardi
Copy link
Copy Markdown
Contributor

Fixes: #14804

Description:
This pull request migrates mobile voice recording from the deprecated expo-av package to expo-audio.

The main problem addressed is that the app was still relying on expo-av APIs for audio recording even though Expo has deprecated that package in favor of expo-audio. During the migration, a few related issues also had to be handled to preserve existing behavior and stability:

Changes:

  • Replaced the manual recorder lifecycle with useAudioRecorder() and useAudioRecorderState(). This was necessary because expo-audio manages recording through hooks rather than new Audio.Recording(). It also lets the component read the duration directly from recorder state instead of manually subscribing to status updates.
  • Updated the permission flow to use getRecordingPermissionsAsync() and requestRecordingPermissionsAsync(). This replaces the old expo-av permission API and keeps the current behavior of checking permissions before starting a recording.
  • Rewrote recordingOptions() to match the expo-audio shape
  • Kept the msleep(500) workaround after a new permission grant. This was retained intentionally to avoid the known iOS race condition where the first recording attempt can fail immediately after the permission dialog. (expo-av Recording Error: This experience is currently in the background, so the audio session could not be activated. expo/expo#21782)
  • After stopping a recording, the file is now obtained from recorder.uri and passed into recordingToSaveData function. This matches the expo-audio API and removes the previous dependence on the old recording object methods
  • Added explicit handling for a missing recording URI, now throws a clear error if the URI is null. This makes failure modes more explicit and prevents downstream code from trying to process an invalid file path.

AI disclosure:
This Pull Request was developed with the assistance of a local LLM. The AI helped clarify the expo-audio documentation, troubleshoot specific bugs encountered during implementation, and refine the final code for better optimization and performance.

Testing:
All tests in packages/app-mobile passed successfully. I’ve also attached a visual test for reference

Registrazione.schermo.2026-03-20.alle.14.47.18.mov

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 20, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b8809a03-2a29-4a7b-b63a-2b5604b8504e

📥 Commits

Reviewing files that changed from the base of the PR and between 06f6c85 and 2205bed.

📒 Files selected for processing (1)
  • packages/app-mobile/jest.setup.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/app-mobile/jest.setup.js

📝 Walkthrough

Walkthrough

Migrated the audio recording functionality from the deprecated expo-av library to expo-audio. Updated the implementation to use new APIs for recording, permissions, and audio mode configuration. Updated corresponding test mocks and dependency declarations to support the new library.

Changes

Cohort / File(s) Summary
Audio Recording Implementation
packages/app-mobile/components/voiceTyping/AudioRecordingBanner.tsx
Replaced expo-av Audio APIs with expo-audio equivalents: migrated Audio.Recording to useAudioRecorder hook, updated permission handling, adjusted recording lifecycle methods (prepareToRecordAsync(), record(), stop()), and updated duration tracking to use recorderStatus.durationMillis. Recording options converted to a constant object with explicit encoder and output fields.
Test Configuration
packages/app-mobile/jest.setup.js
Added dedicated Jest mock for expo-audio with exports for AudioQuality and IOSOutputFormat, stubbed async methods for permissions and audio mode, and removed expo-av from the blanket empty-mock list.
Dependencies & Configuration
packages/app-mobile/package.json, renovate.json5
Replaced expo-av (v16.0.8) with expo-audio (v1.1.1) in package dependencies. Updated Renovate ignore list to exclude expo-audio instead of expo-av from automated updates.

Suggested reviewers

  • personalizedrefrigerator
🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: migrating from expo-av to expo-audio for mobile voice recording, and directly references the linked issue #14804.
Description check ✅ Passed The description is directly related to the changeset, providing detailed explanation of the migration from expo-av to expo-audio, the rationale, and specific implementation changes.
Linked Issues check ✅ Passed The pull request successfully addresses all coding requirements from issue #14804: replaces expo-av APIs with expo-audio equivalents, implements hook-based recorder (useAudioRecorder/useAudioRecorderState), updates permissions (getRecordingPermissionsAsync/requestRecordingPermissionsAsync), converts recording options and save flow, and adds explicit URI error handling.
Out of Scope Changes check ✅ Passed All changes are directly related to the migration objective: component refactoring for expo-audio, Jest setup for testing support, package.json dependency update, and Renovate configuration update. No unrelated changes detected.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Pr Description Must Follow Guidelines ✅ Passed PR description includes all required sections: problem statement (expo-av deprecation), solution explanation (migration with API transitions, hooks, permissions, file handling), and test plan (all tests passed locally with visual assets).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai bot added bug It's a bug enhancement Feature requests and code enhancements mobile All mobile platforms Voice typing labels Mar 20, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/app-mobile/components/voiceTyping/AudioRecordingBanner.tsx (1)

108-108: Consider memoising recording options.

recordingOptions() is called on every render, creating a new object each time. If useExpoAudioRecorder compares options by reference, this could cause unnecessary re-initialisations. Consider using useMemo:

♻️ Suggested refactor
+const options = useMemo(() => recordingOptions(), []);
-const recorder = useExpoAudioRecorder(recordingOptions());
+const recorder = useExpoAudioRecorder(options);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/app-mobile/components/voiceTyping/AudioRecordingBanner.tsx` at line
108, The recordingOptions() call is creating a new object each render and may
trigger unnecessary re-initialisations in useExpoAudioRecorder; wrap the options
in useMemo (e.g., const memoRecordingOptions = useMemo(() => recordingOptions(),
[/* any deps like language/sampleRate */])) and pass memoRecordingOptions to
useExpoAudioRecorder so the reference is stable; update AudioRecordingBanner.tsx
to import useMemo if needed and ensure the dependency array includes any values
recordingOptions depends on.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/app-mobile/components/voiceTyping/AudioRecordingBanner.tsx`:
- Around line 139-151: The recorder can be left prepared if an exception happens
after prepareToRecordAsync() but before isRecordingRef.current is set; to fix,
set isRecordingRef.current = true immediately after prepareToRecordAsync()
(before recorder.record()) or add explicit cleanup in the catch block that
checks the recorder's prepared state and calls
recorder.stop()/recorder.unloadAsync() before clearing isRecordingRef; update
the code paths that setRecordingState(RecorderState.Error) and setError(...)
accordingly so the prepared recorder is always stopped/unloaded (reference
prepareToRecordAsync, isRecordingRef, recorder.record, recorder.stop,
setRecordingState, setError).

---

Nitpick comments:
In `@packages/app-mobile/components/voiceTyping/AudioRecordingBanner.tsx`:
- Line 108: The recordingOptions() call is creating a new object each render and
may trigger unnecessary re-initialisations in useExpoAudioRecorder; wrap the
options in useMemo (e.g., const memoRecordingOptions = useMemo(() =>
recordingOptions(), [/* any deps like language/sampleRate */])) and pass
memoRecordingOptions to useExpoAudioRecorder so the reference is stable; update
AudioRecordingBanner.tsx to import useMemo if needed and ensure the dependency
array includes any values recordingOptions depends on.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 08beb34b-eed4-4769-8560-527f4c6ad179

📥 Commits

Reviewing files that changed from the base of the PR and between 064e72c and 0a68009.

⛔ Files ignored due to path filters (2)
  • packages/app-mobile/ios/Podfile.lock is excluded by !**/*.lock
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (4)
  • packages/app-mobile/components/voiceTyping/AudioRecordingBanner.tsx
  • packages/app-mobile/jest.setup.js
  • packages/app-mobile/package.json
  • renovate.json5
💤 Files with no reviewable changes (1)
  • packages/app-mobile/jest.setup.js

@gherardi gherardi changed the title Mobile: Fix #14804: Migrate expo av to expo audio Mobile: Fix #14804: Migrate expo-av to expo-audio Mar 20, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/app-mobile/components/voiceTyping/AudioRecordingBanner.tsx (1)

38-45: Remove linearPCM options unnecessary for MPEG4AAC encoding.

The linearPCMBitDepth, linearPCMIsBigEndian, and linearPCMIsFloat properties are documented as specific to LINEARPCM output format, not MPEG4AAC. These should be removed from the iOS recording options to avoid confusion and improve code clarity.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/app-mobile/components/voiceTyping/AudioRecordingBanner.tsx` around
lines 38 - 45, In AudioRecordingBanner.tsx update the ios recording options
object used for iOS (the ios constant passed to the recorder) by removing the
LINEARPCM-specific properties linearPCMBitDepth, linearPCMIsBigEndian, and
linearPCMIsFloat since the outputFormat is IOSOutputFormat.MPEG4AAC; leave
extension, audioQuality, and outputFormat as-is so the iOS options only include
settings relevant to MPEG4AAC.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/app-mobile/components/voiceTyping/AudioRecordingBanner.tsx`:
- Around line 95-102: The resetAudioMode function calls setAudioModeAsync with
an invalid AudioMode property; remove the non-existent allowsBackgroundRecording
key from the object passed to setAudioModeAsync (leave allowsRecording,
playsInSilentMode, shouldPlayInBackground and any other valid AudioMode props
intact) so resetAudioMode and its call to setAudioModeAsync use only supported
expo-audio AudioMode parameters.

---

Nitpick comments:
In `@packages/app-mobile/components/voiceTyping/AudioRecordingBanner.tsx`:
- Around line 38-45: In AudioRecordingBanner.tsx update the ios recording
options object used for iOS (the ios constant passed to the recorder) by
removing the LINEARPCM-specific properties linearPCMBitDepth,
linearPCMIsBigEndian, and linearPCMIsFloat since the outputFormat is
IOSOutputFormat.MPEG4AAC; leave extension, audioQuality, and outputFormat as-is
so the iOS options only include settings relevant to MPEG4AAC.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d1cafecd-78c0-4a44-92e3-eb4fe8cc1534

📥 Commits

Reviewing files that changed from the base of the PR and between 0a68009 and 4033ba3.

📒 Files selected for processing (1)
  • packages/app-mobile/components/voiceTyping/AudioRecordingBanner.tsx

@personalizedrefrigerator
Copy link
Copy Markdown
Collaborator

personalizedrefrigerator commented Mar 20, 2026

Thank you for working on this!

All tests in packages/app-mobile passed successfully.

The Note.test.tsx tests are failing in CI:

[107](https://github.com/laurent22/joplin/actions/runs/23347555025/job/67917023266?pr=14847#step:7:1111)
[@joplin/app-mobile]: FAIL components/screens/Note/Note.test.tsx
[@joplin/app-mobile]:   ● Test suite failed to run
[@joplin/app-mobile]: 
[@joplin/app-mobile]:     node_modules/expo-modules-core/src/requireNativeModule.ts:39:7 - error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ ExpoAsset?: ExpoAssetModule; ExpoFontLoader?: ExpoFontLoaderModule; ExpoFontUtils?: ExpoFontUtilsModule; }'.
[@joplin/app-mobile]:       No index signature with a parameter of type 'string' was found on type '{ ExpoAsset?: ExpoAssetModule; ExpoFontLoader?: ExpoFontLoaderModule; ExpoFontUtils?: ExpoFontUtilsModule; }'.
[@joplin/app-mobile]: 
[@joplin/app-mobile]:     39       globalThis.expo?.modules?.[moduleName] ??
[@joplin/app-mobile]:              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[@joplin/app-mobile]: 
[@joplin/app-mobile]:

It may be necessary to update jest.setup.js in packages/app-mobile with new mocks.

@personalizedrefrigerator
Copy link
Copy Markdown
Collaborator

Note: The code changes look good to me! However, I plan to test this locally on web and Android before marking this as ready for review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug It's a bug enhancement Feature requests and code enhancements mobile All mobile platforms Voice typing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Mobile: Migrate away from deprecated expo-av to expo-audio

2 participants