refactor(mobile): drop the unused playback path from the dictation audio module (#39293)

This commit is contained in:
Kelvin Amoaba
2026-11-07 05:03:22 +00:00
committed by GitHub
parent c912144aae
commit df38124cce
10 changed files with 22 additions and 367 deletions
+3 -20
View File
@@ -1,13 +1,12 @@
# Orca Two Way Audio
Vendored Expo module for capturing and playing PCM audio data in the Orca mobile app (iOS and Android).
Vendored Expo module for capturing PCM microphone audio in the Orca mobile app (iOS and Android). Orca uses it for voice dictation; the upstream playback path was removed because nothing played audio through it.
The aim of the module is to facilitate creating real-time conversational apps. The following features are provided:
The following features are provided:
- Request audio recording permissions
- Get clean (applying Acoustic Echo Cancelling) microphone samples in PCM format (1 channel 16 bit at 16kHz)
- Play audio samples in PCM format (1 channel 16 bit at 16kHz). Playback happens through main speaker unless external audio sources are connected.
- Provide volume level both for the input and output samples. Float between 0 and 1.
- Provide the input volume level. Float between 0 and 1.
- [iOS only] Get microphone mode and prompt user to select a microphone mode.
## Installation
@@ -39,22 +38,6 @@ npm i @orca/expo-two-way-audio
```
1. Play audio
> [!NOTE]
> The sample below uses the `buffer` module:
> `npm install buffer`
```JSX
import { Buffer } from "buffer";
// As an example, play pcm data hardcoded in a variable.
const audioChunk = "SOME PCM DATA BASE64 ENCODED HERE"
const buffer = Buffer.from(audioChunk, "base64");
const pcmData = new Uint8Array(buffer);
playPCMData(pcmData);
```
1. Get microphone samples
```JSX
@@ -8,7 +8,6 @@ import android.media.AudioFocusRequest
import android.media.AudioFormat
import android.media.AudioManager
import android.media.AudioRecord
import android.media.AudioTrack
import android.media.MediaRecorder
import android.media.audiofx.AcousticEchoCanceler
import android.media.audiofx.NoiseSuppressor
@@ -16,10 +15,7 @@ import android.os.Build
import android.os.PowerManager
import android.util.Log
import androidx.annotation.RequiresApi
import java.util.Queue
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.math.pow
@@ -30,32 +26,22 @@ class AudioEngine (context: Context) {
private lateinit var audioRecord: AudioRecord
private lateinit var audioManager: AudioManager
private lateinit var audioTrack: AudioTrack
private var audioFocusRequest: AudioFocusRequest? = null
private var audioFocusChangeListener: AudioManager.OnAudioFocusChangeListener? = null
private var audioDeviceCallback: android.media.AudioDeviceCallback? = null
private val audioSampleQueue: Queue<ByteArray> = ConcurrentLinkedQueue()
private val playbackRunning = AtomicBoolean(false)
private var echoCanceler: AcousticEchoCanceler? = null
private var noiseSuppressor: NoiseSuppressor? = null
private val executorServiceMicrophone = Executors.newFixedThreadPool(1)
private val executorServicePlayback = Executors.newFixedThreadPool(1)
private var speakerDevice: AudioDeviceInfo? = null
private var bridgeWindowStartedAtMs = System.currentTimeMillis()
private var micEvents = 0
private var micBytes = 0L
private var playbackEvents = 0
private var playbackQueuedBytes = 0L
private var playbackWrites = 0
private var playbackWriteBytes = 0L
var isRecording = false
private var isRecordingBeforePause = false
var isPlaying = false
var onMicDataCallback: ((ByteArray) -> Unit)? = null
var onInputVolumeCallback: ((Float) -> Unit)? = null
var onOutputVolumeCallback: ((Float) -> Unit)? = null
var onAudioInterruptionCallback: ((String) -> Unit)? = null
init {
@@ -70,18 +56,11 @@ class AudioEngine (context: Context) {
}
Log.d(
"AudioEngine",
"[bridge] $reason mic=$micEvents" +
"ev/${micBytes}B playbackQueue=$playbackEvents" +
"ev/${playbackQueuedBytes}B playbackWrites=$playbackWrites" +
"ev/${playbackWriteBytes}B windowMs=$elapsedMs"
"[bridge] $reason mic=${micEvents}ev/${micBytes}B windowMs=$elapsedMs"
)
bridgeWindowStartedAtMs = now
micEvents = 0
micBytes = 0
playbackEvents = 0
playbackQueuedBytes = 0
playbackWrites = 0
playbackWriteBytes = 0
}
@SuppressLint("NewApi")
@@ -111,29 +90,6 @@ class AudioEngine (context: Context) {
} else {
updateLegacyAudioRouting()
}
val bufferSize = AudioTrack.getMinBufferSize(
SAMPLE_RATE,
AudioFormat.CHANNEL_OUT_MONO,
AUDIO_FORMAT
)
audioTrack = AudioTrack(
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build(),
AudioFormat.Builder()
.setEncoding(AUDIO_FORMAT)
.setSampleRate(SAMPLE_RATE)
.setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
.build(),
bufferSize,
AudioTrack.MODE_STREAM,
audioManager.generateAudioSessionId()
).apply {
play()
}
}
@RequiresApi(Build.VERSION_CODES.M)
@@ -195,10 +151,9 @@ class AudioEngine (context: Context) {
AudioManager.AUDIOFOCUS_LOSS -> {
Log.d("AudioEngine", "Audio focus lost")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
pauseRecordingAndPlayer()
pauseRecording()
} else {
stopRecording()
stopPlayback()
// Pre-Q stops for good rather than pausing, so the focus goes back too.
abandonAudioFocus()
}
@@ -339,47 +294,6 @@ class AudioEngine (context: Context) {
return isRecording
}
fun playPCMData(data: ByteArray) {
audioSampleQueue.add(data)
playbackEvents += 1
playbackQueuedBytes += data.size.toLong()
flushBridgeStats("queue")
if (playbackRunning.compareAndSet(false, true)) {
playAudioFromSampleQueue()
}
}
private fun playAudioFromSampleQueue() {
executorServicePlayback.execute{
isPlaying = true
try {
while (true){
val data = audioSampleQueue.poll() ?: break
playSample(data)
val audioVolume = calculateRMSLevel(data)
onOutputVolumeCallback?.invoke(audioVolume)
}
}catch (e: Exception){
Log.e("AudioEngine", "Error playing audio", e)
e.printStackTrace()
}finally {
playbackRunning.set(false)
isPlaying = false
onOutputVolumeCallback?.invoke(0.0F)
if (audioSampleQueue.isNotEmpty() && playbackRunning.compareAndSet(false, true)) {
playAudioFromSampleQueue()
}
}
}
}
private fun playSample(data: ByteArray) {
val written = audioTrack.write(data, 0, data.size)
playbackWrites += 1
playbackWriteBytes += written.coerceAtLeast(0).toLong()
flushBridgeStats("write")
}
fun bypassVoiceProcessing(bypass: Boolean) {
if (bypass) {
echoCanceler?.enabled = false
@@ -391,14 +305,13 @@ class AudioEngine (context: Context) {
}
@RequiresApi(Build.VERSION_CODES.Q)
fun pauseRecordingAndPlayer() {
fun pauseRecording() {
isRecordingBeforePause = isRecording
isRecording = toggleRecording(false, clearPauseResume = false)
audioTrack.pause()
}
@RequiresApi(Build.VERSION_CODES.Q)
fun resumeRecordingAndPlayer() {
fun resumeRecording() {
requestAudioFocus()
// Consume the flag: a second resume without an intervening pause must not reopen the microphone.
val shouldResumeRecording = isRecordingBeforePause
@@ -407,42 +320,15 @@ class AudioEngine (context: Context) {
if (shouldResumeRecording) {
isRecording = toggleRecording(true)
}
audioTrack.play()
}
fun stopPlayback() {
audioSampleQueue.clear()
audioTrack.pause()
audioTrack.flush()
playbackRunning.set(false)
isPlaying = false
onOutputVolumeCallback?.invoke(0.0F)
Log.d("AudioEngine", "Playback stopped")
}
fun pausePlayback() {
audioTrack.pause()
Log.d("AudioEngine", "Playback paused")
}
fun resumePlayback() {
audioTrack.play()
Log.d("AudioEngine", "Playback resumed")
}
@SuppressLint("NewApi")
fun tearDown() {
stopRecording()
if (::audioTrack.isInitialized) {
audioTrack.stop()
audioTrack.release()
}
echoCanceler?.release()
echoCanceler = null
noiseSuppressor?.release()
noiseSuppressor = null
audioSampleQueue.clear()
playbackRunning.set(false)
audioManager.mode = AudioManager.MODE_NORMAL
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
audioDeviceCallback?.let { audioManager.unregisterAudioDeviceCallback(it) }
@@ -453,7 +339,6 @@ class AudioEngine (context: Context) {
}
abandonAudioFocus()
executorServiceMicrophone.shutdownNow()
executorServicePlayback.shutdownNow()
}
@@ -6,13 +6,13 @@ import expo.modules.core.interfaces.ReactActivityLifecycleListener
class ExpoTwoWayAudioLifeCycleListener : ReactActivityLifecycleListener {
override fun onPause(activity: Activity?) {
super.onPause(activity)
// At the moment background audio recording and playback is not supported
ExpoTwoWayAudioModule.audioEngine?.pauseRecordingAndPlayer()
// At the moment background audio recording is not supported
ExpoTwoWayAudioModule.audioEngine?.pauseRecording()
}
override fun onResume(activity: Activity?) {
super.onResume(activity)
// At the moment background audio recording and playback is not supported
ExpoTwoWayAudioModule.audioEngine?.resumeRecordingAndPlayer()
// At the moment background audio recording is not supported
ExpoTwoWayAudioModule.audioEngine?.resumeRecording()
}
}
@@ -10,7 +10,6 @@ class ExpoTwoWayAudioModule : Module() {
companion object {
private const val ON_MIC_DATA_EVENT = "onMicrophoneData"
private const val ON_INPUT_VOLUME_LEVEL_EVENT = "onInputVolumeLevelData"
private const val ON_OUTPUT_VOLUME_LEVEL_EVENT = "onOutputVolumeLevelData"
private const val ON_RECORDING_CHANGE_EVENT = "onRecordingChange"
private const val ON_AUDIO_INTERRUPTION_EVENT = "onAudioInterruption"
var audioEngine: AudioEngine? = null
@@ -51,36 +50,16 @@ class ExpoTwoWayAudioModule : Module() {
}
Function("restart") {
audioEngine?.resumeRecordingAndPlayer()
audioEngine?.resumeRecording()
sendEvent(ON_RECORDING_CHANGE_EVENT, mapOf(
"data" to (audioEngine?.isRecording ?: false)
))
}
Function("playPCMData") { data: kotlin.ByteArray ->
audioEngine?.playPCMData(data)
}
Function("bypassVoiceProcessing") { bypass: Boolean ->
audioEngine?.bypassVoiceProcessing(bypass)
}
Function("isPlaying") {
audioEngine?.isPlaying ?: false
}
Function("stopPlayback") {
audioEngine?.stopPlayback()
}
Function("pausePlayback") {
audioEngine?.pausePlayback()
}
Function("resumePlayback") {
audioEngine?.resumePlayback()
}
Function("getMicrophoneModeIOS") {
throw UnsupportedOperationException("getMicrophoneModeIOS is only supported on iOS")
}
@@ -118,7 +97,6 @@ class ExpoTwoWayAudioModule : Module() {
Events(
ON_MIC_DATA_EVENT,
ON_INPUT_VOLUME_LEVEL_EVENT,
ON_OUTPUT_VOLUME_LEVEL_EVENT,
ON_RECORDING_CHANGE_EVENT,
ON_AUDIO_INTERRUPTION_EVENT
)
@@ -132,9 +110,6 @@ class ExpoTwoWayAudioModule : Module() {
onInputVolumeCallback = { level ->
sendEvent(ON_INPUT_VOLUME_LEVEL_EVENT, bundleOf("data" to level))
}
onOutputVolumeCallback = { level ->
sendEvent(ON_OUTPUT_VOLUME_LEVEL_EVENT, bundleOf("data" to level))
}
onAudioInterruptionCallback = { data ->
sendEvent(ON_AUDIO_INTERRUPTION_EVENT, bundleOf("data" to data))
sendEvent(ON_RECORDING_CHANGE_EVENT, bundleOf(
@@ -3,7 +3,6 @@ import Foundation
class AudioEngine {
private var avAudioEngine = AVAudioEngine()
private var speechPlayer = AVAudioPlayerNode()
private var engineConfigChangeObserver: Any?
private var sessionInterruptionObserver: Any?
private var mediaServicesResetObserver: Any?
@@ -15,28 +14,16 @@ class AudioEngine {
public var onMicDataCallback: ((Data) -> Void)?
public var onInputVolumeCallback: ((Float) -> Void)?
public var onOutputVolumeCallback: ((Float) -> Void)?
public var onAudioInterruptionCallback: ((String) -> Void)?
private var inputLevelTimer: Timer?
private var outputLevelTimer: Timer?
private var inputBuffer = [Float](repeating: 0, count: 2048)
private var outputBuffer = [Float](repeating: 0, count: 2048)
private var inputBufferIndex = 0
private var outputBufferIndex = 0
private var hasFirstInputBeenDiscarded = false
private var discardRecording = false
private var discardFirstInputMillis = 2000
enum AudioEngineError: Error {
case audioFormatError
}
init() throws {
avAudioEngine.attach(speechPlayer)
guard let format = AVAudioFormat(standardFormatWithSampleRate: 16000, channels: 1) else {
throw AudioEngineError.audioFormatError
}
@@ -121,25 +108,16 @@ class AudioEngine {
avAudioEngine.inputNode.isVoiceProcessingInputMuted = !isRecording
let output = avAudioEngine.outputNode
let mainMixer = avAudioEngine.mainMixerNode
avAudioEngine.connect(speechPlayer, to: mainMixer, format: voiceIOFormat)
avAudioEngine.connect(mainMixer, to: output, format: voiceIOFormat)
// Voice processing runs input and output as one unit, so keep the output side wired even though nothing plays.
avAudioEngine.connect(avAudioEngine.mainMixerNode, to: avAudioEngine.outputNode, format: voiceIOFormat)
input.installTap(onBus: 0, bufferSize: 2048, format: voiceIOFormat) { [weak self] buffer, when in
// Skip mic processing while playback’s echo canceller adapts; see playPCMData.
if self?.isRecording == true && self?.discardRecording == false {
if self?.isRecording == true {
self?.processMicrophoneBuffer(buffer)
self?.updateInputVolume()
}
}
mainMixer.installTap(onBus: 0, bufferSize: 2048, format: voiceIOFormat) { [weak self] buffer, when in
self?.processOutputBuffer(buffer)
self?.updateOutputVolume()
}
avAudioEngine.prepare()
}
@@ -165,21 +143,6 @@ class AudioEngine {
onMicDataCallback?(data)
}
func processOutputBuffer(_ buffer: AVAudioPCMBuffer) {
guard let channelData = buffer.floatChannelData?[0] else {
print("Error: Could not access channel data")
return
}
let frameCount = Int(buffer.frameLength)
for i in 0..<frameCount {
let floatSample = max(-1.0, min(1.0, channelData[i]))
outputBuffer[outputBufferIndex] = floatSample
outputBufferIndex = (outputBufferIndex + 1) % outputBuffer.count
}
}
func start() {
guard !isTornDown else { return }
do {
@@ -189,55 +152,6 @@ class AudioEngine {
}
}
func playPCMData(_ pcmData: Data) {
guard !isTornDown else { return }
// AEC misses the first playback chunks; briefly discard mic callbacks (without muting) while it adapts.
if !hasFirstInputBeenDiscarded {
self.hasFirstInputBeenDiscarded = true
self.discardRecording = true
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(discardFirstInputMillis)) {
self.discardRecording = false
}
}
guard let buffer = createBuffer(from: pcmData) else {
print("Failed to create audio buffer")
return
}
speechPlayer.scheduleBuffer(buffer)
if !speechPlayer.isPlaying {
speechPlayer.play()
}
}
private func createBuffer(from data: Data) -> AVAudioPCMBuffer? {
let frameCount = UInt32(data.count) / 2 // 16-bit input = 2 bytes per frame
let format = AVAudioFormat(commonFormat: .pcmFormatFloat32,
sampleRate: 16000,
channels: 1,
interleaved: false)!
guard let buffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: frameCount) else {
return nil
}
buffer.frameLength = frameCount
data.withUnsafeBytes { (rawBufferPointer: UnsafeRawBufferPointer) in
if let sourcePtr = rawBufferPointer.baseAddress?.assumingMemoryBound(to: Int16.self),
let destPtr = buffer.floatChannelData?[0] {
for i in 0..<Int(frameCount) {
destPtr[i] = Float(sourcePtr[i]) / Float(Int16.max)
}
}
}
return buffer
}
func bypassVoiceProcessing(_ bypass: Bool) {
let input = avAudioEngine.inputNode
input.isVoiceProcessingBypassed = bypass
@@ -276,22 +190,13 @@ class AudioEngine {
return isRecording
}
func stopRecordingAndPlayer(clearInterruptionResume: Bool = true){
func stopRecording(clearInterruptionResume: Bool = true){
do {
try AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
} catch {
print("Could not set the audio session to inactive: \(error)")
}
toggleRecording(false, preservingInterruptionIntent: !clearInterruptionResume)
speechPlayer.stop()
updateOutputVolume()
}
@discardableResult
func resumeRecordingAndPlayer() -> Bool {
guard toggleRecording(true) else { return false }
speechPlayer.play()
return true
}
func tearDown() {
@@ -299,31 +204,7 @@ class AudioEngine {
isTornDown = true
// Stop I/O first; deactivating a session with running I/O returns a busy error.
avAudioEngine.stop()
stopRecordingAndPlayer()
}
var isPlaying: Bool {
return speechPlayer.isPlaying
}
func stopPlayback() {
speechPlayer.stop()
outputBuffer = [Float](repeating: 0, count: 2048)
updateOutputVolume()
print("Playback stopped")
}
func pausePlayback() {
speechPlayer.pause()
print("Playback paused")
}
func resumePlayback() {
guard !isTornDown else { return }
if !speechPlayer.isPlaying {
speechPlayer.play()
print("Playback resumed")
}
stopRecording()
}
private func checkEngineIsRunning() {
@@ -342,14 +223,14 @@ class AudioEngine {
switch type {
case .began:
interruptionIntent.began(wasRecording: isRecording)
self.stopRecordingAndPlayer(clearInterruptionResume: false)
self.stopRecording(clearInterruptionResume: false)
onAudioInterruptionCallback?("began")
case .ended:
let rawOptions = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt ?? 0
let shouldResume = AVAudioSession.InterruptionOptions(rawValue: rawOptions).contains(.shouldResume)
switch interruptionIntent.ended(shouldResume: shouldResume, isRecording: isRecording) {
case .resume:
let resumed = resumeRecordingAndPlayer()
let resumed = toggleRecording(true)
onAudioInterruptionCallback?(resumed ? "ended" : "blocked")
case .blocked:
onAudioInterruptionCallback?("blocked")
@@ -357,7 +238,7 @@ class AudioEngine {
break // A newer explicit start already owns the microphone.
}
@unknown default:
stopRecordingAndPlayer()
stopRecording()
onAudioInterruptionCallback?("blocked")
}
}
@@ -381,11 +262,6 @@ class AudioEngine {
onInputVolumeCallback?(volume)
}
private func updateOutputVolume() {
let volume = calculateRMSLevel(from: outputBuffer)
onOutputVolumeCallback?(volume)
}
private func calculateRMSLevel(from buffer: [Float]) -> Float {
let epsilon: Float = 1e-5 // To avoid log(0)
let rmsValue = sqrt(buffer.reduce(0) { $0 + $1 * $1 } / Float(buffer.count))
@@ -3,7 +3,6 @@ import AVFoundation
let ON_MIC_DATA_EVENT_NAME = "onMicrophoneData"
let ON_INPUT_VOLUME_LEVEL_EVENT_NAME = "onInputVolumeLevelData"
let ON_OUTPUT_VOLUME_LEVEL_EVENT_NAME = "onOutputVolumeLevelData"
let ON_RECORDING_CHANGE_EVENT_NAME = "onRecordingChange"
let ON_AUDIO_INTERRUPTION_EVENT_NAME = "onAudioInterruption"
@@ -31,7 +30,6 @@ public class ExpoTwoWayAudioModule: Module {
self.audioEngine = try AudioEngine()
self.setupMicrophoneCallback()
self.setupInputAudioLevelCallback()
self.setupOutputAudioLevelCallback()
self.setupAudioInterruptionCallback()
return true
} catch {
@@ -98,7 +96,7 @@ public class ExpoTwoWayAudioModule: Module {
}
Function("restart") {
self.audioEngine?.resumeRecordingAndPlayer()
_ = self.audioEngine?.toggleRecording(true)
self.sendEvent(
ON_RECORDING_CHANGE_EVENT_NAME,
[
@@ -107,30 +105,10 @@ public class ExpoTwoWayAudioModule: Module {
}
Function("playPCMData") { (pcmData: Data) in
self.audioEngine?.playPCMData(pcmData)
}
Function("bypassVoiceProcessing") { (bypass: Bool) in
self.audioEngine?.bypassVoiceProcessing(bypass)
}
Function("isPlaying") { () -> Bool in
return self.audioEngine?.isPlaying ?? false
}
Function("stopPlayback") {
self.audioEngine?.stopPlayback()
}
Function("pausePlayback") {
self.audioEngine?.pausePlayback()
}
Function("resumePlayback") {
self.audioEngine?.resumePlayback()
}
AsyncFunction("getMicrophonePermissionsAsync") { (promise: Promise) in
EXPermissionsMethodsDelegate.getPermissionWithPermissionsManager(
self.appContext?.permissions,
@@ -152,7 +130,6 @@ public class ExpoTwoWayAudioModule: Module {
Events([
ON_MIC_DATA_EVENT_NAME,
ON_INPUT_VOLUME_LEVEL_EVENT_NAME,
ON_OUTPUT_VOLUME_LEVEL_EVENT_NAME,
ON_RECORDING_CHANGE_EVENT_NAME,
ON_AUDIO_INTERRUPTION_EVENT_NAME,
])
@@ -178,16 +155,6 @@ public class ExpoTwoWayAudioModule: Module {
}
}
private func setupOutputAudioLevelCallback() {
audioEngine?.onOutputVolumeCallback = { [weak self] level in
self?.sendEvent(
ON_OUTPUT_VOLUME_LEVEL_EVENT_NAME,
[
"data": level
])
}
}
private func setupAudioInterruptionCallback() {
audioEngine?.onAudioInterruptionCallback = { [weak self] data in
self?.sendEvent(
@@ -6,7 +6,6 @@ type EventSubscription = {
type ExpoTwoWayAudioWebModule = {
initialize: () => Promise<boolean>
playPCMData: (audioData: Uint8Array) => void
bypassVoiceProcessing: (bypass: boolean) => void
toggleRecording: (val: boolean) => boolean
isRecording: () => boolean
@@ -16,10 +15,6 @@ type ExpoTwoWayAudioWebModule = {
requestMicrophonePermissionsAsync: () => Promise<PermissionResponse>
getMicrophoneModeIOS: () => null
setMicrophoneModeIOS: () => void
isPlaying: () => boolean
stopPlayback: () => void
pausePlayback: () => void
resumePlayback: () => void
addListener: (eventName: string, handler: (ev: unknown) => void) => EventSubscription
}
@@ -35,7 +30,6 @@ const noop = () => undefined
const ExpoTwoWayAudioModule: ExpoTwoWayAudioWebModule = {
// QA browser has no shell microphone; page dictation uses platform/dictation-capture.web.ts native.audio.start|read|stop instead.
initialize: async () => false,
playPCMData: noop,
bypassVoiceProcessing: noop,
toggleRecording: () => false,
isRecording: () => false,
@@ -45,10 +39,6 @@ const ExpoTwoWayAudioModule: ExpoTwoWayAudioWebModule = {
requestMicrophonePermissionsAsync: async () => deniedMicrophonePermission,
getMicrophoneModeIOS: () => null,
setMicrophoneModeIOS: noop,
isPlaying: () => false,
stopPlayback: noop,
pausePlayback: noop,
resumePlayback: noop,
addListener: () => ({ remove: noop })
}
@@ -5,10 +5,6 @@ export async function initialize() {
return await ExpoTwoWayAudioModule.initialize()
}
export function playPCMData(audioData: Uint8Array) {
return ExpoTwoWayAudioModule.playPCMData(audioData)
}
export function bypassVoiceProcessing(bypass: boolean) {
return ExpoTwoWayAudioModule.bypassVoiceProcessing(bypass)
}
@@ -44,19 +40,3 @@ export function getMicrophoneModeIOS() {
export function setMicrophoneModeIOS() {
return ExpoTwoWayAudioModule.setMicrophoneModeIOS()
}
export function isPlaying(): boolean {
return ExpoTwoWayAudioModule.isPlaying()
}
export function stopPlayback() {
return ExpoTwoWayAudioModule.stopPlayback()
}
export function pausePlayback() {
return ExpoTwoWayAudioModule.pausePlayback()
}
export function resumePlayback() {
return ExpoTwoWayAudioModule.resumePlayback()
}
@@ -19,7 +19,6 @@ export interface AudioInterruptionEvent {
export interface ExpoTwoWayAudioEventMap {
onMicrophoneData: MicrophoneDataEvent
onInputVolumeLevelData: VolumeLevelEvent
onOutputVolumeLevelData: VolumeLevelEvent
onRecordingChange: RecordingChangeEvent
onAudioInterruption: AudioInterruptionEvent
}
+1 -1
View File
@@ -15,7 +15,7 @@
},
{
"file": "packages/expo-two-way-audio/src/ExpoTwoWayAudioModule.web.ts",
"reason": "Vendored with the module, not added for Route A. Nothing on the page reaches it any more: dictation's capture went behind src/platform/dictation-capture.web.ts, which asks the shell for the microphone, so the only importer of @orca/expo-two-way-audio is the native half of that seam. The file stays because it ships with the vendored module, and it answers denied microphone permission and no playback, which is what a browser outside the shell can honestly say."
"reason": "Vendored with the module, not added for Route A. Nothing on the page reaches it any more: dictation's capture went behind src/platform/dictation-capture.web.ts, which asks the shell for the microphone, so the only importer of @orca/expo-two-way-audio is the native half of that seam. The file stays because it ships with the vendored module, and it answers denied microphone permission, which is what a browser outside the shell can honestly say."
},
{
"file": "src/navigation/use-back-claim.web.ts",