You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React Native Expo Go iOS端音频音量过小问题求助

解决Expo-Go iOS真机音频音量过小问题

问题背景

在Expo-Go iOS真机上测试应用时,录音及下载音频的播放音量仅为正常水平的一半甚至更低,远低于设备上其他应用(如YouTube)的音量表现。但网页版、Android设备、iOS模拟器及模拟器中Safari打开的网页版,音频均能正常播放。测试代码包含录音、播放录音、播放示例音频三个功能按钮。

解决方案

问题根源是Expo默认将音频输出切换到了音量较小的听筒,只需调整Audio.setAudioModeAsync中的allowsRecordingIOS参数即可解决:

当你需要播放音频时,需将allowsRecordingIOS设置为false。这是Expo中覆盖音频默认从音量较小的听筒输出、改为从扬声器输出的方法。——Gordon Childs

具体修改逻辑:

  • 初始配置音频时,将allowsRecordingIOS设为false,确保默认从扬声器输出音频
  • 如果需要进行录音操作,在开始录音前动态将该参数设为true,录音结束后再改回false以恢复扬声器播放

修改后的关键代码片段

初始音频配置修改

useEffect(() => {
  const setupAudio = async () => {
    try {
      await Audio.requestPermissionsAsync();
      await Audio.setAudioModeAsync({
        allowsRecordingIOS: false, // 核心修改:切换到扬声器输出
        playsInSilentModeIOS: true,
        staysActiveInBackground: true,
        interruptionModeIOS: InterruptionModeIOS.DuckOthers,
        interruptionModeAndroid: InterruptionModeAndroid.DuckOthers,
        shouldDuckAndroid: true,
        playThroughEarpieceAndroid: true
      });
    } catch (error) {
      Alert.alert('Error', 'Failed to initialize audio recording');
    }
  };

  setupAudio();

  return () => {
    if (sound) sound.unloadAsync();
    if (recording) recording.stopAndUnloadAsync();
  };
}, []);

录音/播放模式动态切换

const startRecording = async () => {
  try {
    // 录音前切换到允许录音的模式
    await Audio.setAudioModeAsync({
      allowsRecordingIOS: true,
      playsInSilentModeIOS: true,
      staysActiveInBackground: true,
      interruptionModeIOS: InterruptionModeIOS.DuckOthers,
      interruptionModeAndroid: InterruptionModeAndroid.DuckOthers,
      shouldDuckAndroid: true,
      playThroughEarpieceAndroid: true
    });
    const { recording: newRecording } = await Audio.Recording.createAsync(
      Audio.RecordingOptionsPresets.HIGH_QUALITY
    );
    setRecording(newRecording);
    setIsRecording(true);
  } catch (error) {
    Alert.alert('Error', 'Failed to start recording');
  }
};

const stopRecording = async () => {
  if (!recording) return;

  try {
    await recording.stopAndUnloadAsync();
    const uri = recording.getURI();
    setRecordingUri(uri || null);
    setIsRecording(false);
    // 录音结束后切换回扬声器播放模式
    await Audio.setAudioModeAsync({
      allowsRecordingIOS: false,
      playsInSilentModeIOS: true,
      staysActiveInBackground: true,
      interruptionModeIOS: InterruptionModeIOS.DuckOthers,
      interruptionModeAndroid: InterruptionModeAndroid.DuckOthers,
      shouldDuckAndroid: true,
      playThroughEarpieceAndroid: true
    });
    if (uri && onRecordingComplete) {
      onRecordingComplete(uri);
    }
  } catch (error) {
    Alert.alert('Error', 'Failed to stop recording');
  }
};

完整修改后的代码

import React, { useState, useEffect } from 'react';
import { View, TouchableOpacity, StyleSheet, Alert } from 'react-native';
import { Audio, InterruptionModeIOS, InterruptionModeAndroid } from 'expo-av';
import { FontAwesome } from '@expo/vector-icons';

interface AudioRecorderProps {
  onRecordingComplete?: (uri: string) => void;
}

export const AudioRecorder: React.FC<AudioRecorderProps> = ({ onRecordingComplete }) => {
  const [recording, setRecording] = useState<Audio.Recording | null>(null);
  const [sound, setSound] = useState<Audio.Sound | null>(null);
  const [isRecording, setIsRecording] = useState(false);
  const [isPlaying, setIsPlaying] = useState(false);
  const [recordingUri, setRecordingUri] = useState<string | null>(null);
  const [isPlayingReference, setIsPlayingReference] = useState(false);

  useEffect(() => {
    const setupAudio = async () => {
      try {
        await Audio.requestPermissionsAsync();
        await Audio.setAudioModeAsync({
          allowsRecordingIOS: false,
          playsInSilentModeIOS: true,
          staysActiveInBackground: true,
          interruptionModeIOS: InterruptionModeIOS.DuckOthers,
          interruptionModeAndroid: InterruptionModeAndroid.DuckOthers,
          shouldDuckAndroid: true,
          playThroughEarpieceAndroid: true
        });
      } catch (error) {
        Alert.alert('Error', 'Failed to initialize audio recording');
      }
    };

    setupAudio();

    return () => {
      if (sound) sound.unloadAsync();
      if (recording) recording.stopAndUnloadAsync();
    };
  }, []);

  const startRecording = async () => {
    try {
      await Audio.setAudioModeAsync({
        allowsRecordingIOS: true,
        playsInSilentModeIOS: true,
        staysActiveInBackground: true,
        interruptionModeIOS: InterruptionModeIOS.DuckOthers,
        interruptionModeAndroid: InterruptionModeAndroid.DuckOthers,
        shouldDuckAndroid: true,
        playThroughEarpieceAndroid: true
      });
      const { recording: newRecording } = await Audio.Recording.createAsync(
        Audio.RecordingOptionsPresets.HIGH_QUALITY
      );
      setRecording(newRecording);
      setIsRecording(true);
    } catch (error) {
      Alert.alert('Error', 'Failed to start recording');
    }
  };

  const stopRecording = async () => {
    if (!recording) return;

    try {
      await recording.stopAndUnloadAsync();
      const uri = recording.getURI();
      setRecordingUri(uri || null);
      setIsRecording(false);
      await Audio.setAudioModeAsync({
        allowsRecordingIOS: false,
        playsInSilentModeIOS: true,
        staysActiveInBackground: true,
        interruptionModeIOS: InterruptionModeIOS.DuckOthers,
        interruptionModeAndroid: InterruptionModeAndroid.DuckOthers,
        shouldDuckAndroid: true,
        playThroughEarpieceAndroid: true
      });
      if (uri && onRecordingComplete) {
        onRecordingComplete(uri);
      }
    } catch (error) {
      Alert.alert('Error', 'Failed to stop recording');
    }
  };

  const playSound = async () => {
    if (!recordingUri) return;

    try {
      if (sound) {
        await sound.unloadAsync();
      }

      const { sound: newSound } = await Audio.Sound.createAsync(
        { uri: recordingUri },
        {
          volume: 1.0,
        }
      );
      setSound(newSound);
      setIsPlaying(true);

      await newSound.playAsync();
      newSound.setOnPlaybackStatusUpdate((status) => {
        if (status && 'didJustFinish' in status && status.didJustFinish) {
          setIsPlaying(false);
        }
      });
    } catch (error) {
      Alert.alert('Error', 'Failed to play recording');
      setIsPlaying(false);
    }
  };

  const playReferenceTone = async () => {
    try {
      const { sound: referenceSound } = await Audio.Sound.createAsync(
        require('../../assets/beep.mp3'),
        {
          volume: 1.0,
        }
      );

      setIsPlayingReference(true);

      await referenceSound.playAsync();
      referenceSound.setOnPlaybackStatusUpdate((status) => {
        if (status && 'didJustFinish' in status && status.didJustFinish) {
          setIsPlayingReference(false);
          referenceSound.unloadAsync();
        }
      });
    } catch (error) {
      Alert.alert('Error', 'Failed to play reference tone');
      setIsPlayingReference(false);
    }
  };

  return (
    <View style={styles.container}>
      <TouchableOpacity
        style={[styles.button, isRecording ? styles.recording : null]}
        onPress={isRecording ? stopRecording : startRecording}
        >
        <FontAwesome
          name={isRecording ? 'stop-circle' : 'microphone'}
          size={24}
          color="white"
        />
      </TouchableOpacity>

      {recordingUri && (
        <TouchableOpacity
          style={[styles.button, isPlaying ? styles.playing : null]}
          onPress={playSound}
          disabled={isPlaying}
          >
          <FontAwesome
            name={isPlaying ? 'pause' : 'play'}
            size={24}
            color="white"
          />
        </TouchableOpacity>
      )}

      <TouchableOpacity
        style={[styles.button, isPlayingReference ? styles.playing : styles.referenceTone]}
        onPress={playReferenceTone}
        disabled={isPlayingReference}
      >
        <FontAwesome
          name="volume-up"
          size={24}
          color="white"
        />
      </TouchableOpacity>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flexDirection: 'row',
    justifyContent: 'center',
    alignItems: 'center',
    gap: 20,
  },
  button: {
    width: 60,
    height: 60,
    borderRadius: 30,
    backgroundColor: '#007AFF',
    justifyContent: 'center',
    alignItems: 'center',
  },
  recording: {
    backgroundColor: '#FF3B30',
  },
  playing: {
    backgroundColor: '#34C759',
  },
  referenceTone: {
    backgroundColor: '#5856D6',
  },
});

export default function AudioTest2() {
  const handleRecordingComplete = (uri: string) => {
    console.log('Recording saved at:', uri);
  };

  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <AudioRecorder onRecordingComplete={handleRecordingComplete} />
    </View>
  );
}

内容的提问来源于stack exchange,提问作者Sven Ostertag

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 23:50:55