Expo React Native项目中sound.play未定义错误排查求助
问题分析与解决方案
核心问题
你遇到的sound.play is not a function错误主要来自两个关键点:
- 选库不匹配:
@react-native-community/audio-toolkit仅用于播放本地/远程音频文件,根本没有AudioGenerator这类实时音频生成API。你代码里的AudioGenerator不属于该库,导致生成的newSound不是具备play/stop方法的音频实例。 - 逻辑漏洞:组件顶层直接调用
sound.play(sound),此时sound初始值是数组[],自然没有play方法;同时重复触发的play/stop操作会引发资源泄漏。
正确解决方案(基于Expo AV)
Expo环境下推荐用expo-av实现实时音频生成与播放,以下是修正后的完整代码:
步骤1:安装依赖
npx expo install expo-av
步骤2:修正代码
import React, { useState, useEffect, useRef } from 'react'; import { View, Text, Slider } from 'react-native'; import { Audio } from 'expo-av'; const FrequencyGenerator = () => { const [frequency, setFrequency] = useState(20); const [freq, setFreq] = useState(20); const soundRef = useRef(null); // 用ref保存音频实例,避免状态更新重复创建 // 生成正弦波并播放 const generateAndPlaySound = async (freq) => { // 清理旧音频实例 if (soundRef.current) { await soundRef.current.stopAsync(); await soundRef.current.unloadAsync(); soundRef.current = null; } const sampleRate = 44100; const duration = 0.5; // 生成0.5秒音频,循环播放实现持续输出 const numSamples = sampleRate * duration; const pcmData = new Float32Array(numSamples); // 生成正弦波PCM数据 for (let i = 0; i < numSamples; i++) { const time = i / sampleRate; pcmData[i] = Math.sin(2 * Math.PI * freq * time) * 0.5; // 0.5限制音量避免失真 } // 转换为WAV格式base64并加载播放 const audioBuffer = await Audio.createAudioAsync( { uri: `data:audio/wav;base64,${encodePCMToWavBase64(pcmData, sampleRate)}` }, { shouldPlay: true, isLooping: true } ); soundRef.current = audioBuffer.sound; }; // 将PCM数据编码为WAV格式base64 const encodePCMToWavBase64 = (pcmData, sampleRate) => { const buffer = new ArrayBuffer(44 + pcmData.length * 2); const view = new DataView(buffer); // 写入WAV文件头 view.setUint8(0, 0x52); view.setUint8(1, 0x49); view.setUint8(2, 0x46); view.setUint8(3, 0x46); view.setUint32(4, 44 + pcmData.length * 2, true); view.setUint8(8, 0x57); view.setUint8(9, 0x41); view.setUint8(10, 0x56); view.setUint8(11, 0x45); view.setUint8(12, 0x66); view.setUint8(13, 0x6D); view.setUint8(14, 0x74); view.setUint8(15, 0x20); view.setUint32(16, 16, true); view.setUint16(20, 1, true); view.setUint16(22, 1, true); view.setUint32(24, sampleRate, true); view.setUint32(28, sampleRate * 2, true); view.setUint16(32, 2, true); view.setUint16(34, 16, true); view.setUint8(36, 0x64); view.setUint8(37, 0x61); view.setUint8(38, 0x74); view.setUint8(39, 0x61); view.setUint32(40, pcmData.length * 2, true); // 将Float32转换为16位整数 for (let i = 0; i < pcmData.length; i++) { const sample = Math.max(-1, Math.min(1, pcmData[i])); const intSample = sample < 0 ? sample * 32768 : sample * 32767; view.setInt16(44 + i * 2, intSample, true); } return btoa(String.fromCharCode(...new Uint8Array(buffer))); }; useEffect(() => { // 请求音频权限 const initAudio = async () => await Audio.requestPermissionsAsync(); initAudio(); // 组件卸载时清理音频资源 return async () => { if (soundRef.current) { await soundRef.current.stopAsync(); await soundRef.current.unloadAsync(); } }; }, []); useEffect(() => { // 频率变化时重新生成音频 generateAndPlaySound(frequency); }, [frequency]); const onSliderValueChange = (value) => { const newFreq = Math.ceil(value); setFrequency(newFreq); setFreq(newFreq); }; return ( <View style={{ flex: 1, justifyContent: 'center', padding: 20 }}> <Slider minimumTrackTintColor="pink" minimumValue={20} maximumValue={20000} onValueChange={onSliderValueChange} value={frequency} /> <Text style={{ textAlign: 'center', marginTop: 20, fontSize: 20 }}>{freq} HZ</Text> </View> ); }; export default FrequencyGenerator;
关键说明
- 库替换:用
expo-av替代原库,它原生支持Expo环境,且能处理自定义音频数据。 - 资源管理:用
useRef保存音频实例,避免状态更新重复创建;频率变化或组件卸载时,及时清理旧实例防止泄漏。 - 循环播放:通过设置
isLooping: true实现持续音频输出,无需重复生成长音频。 - 权限处理:初始化时请求音频权限,确保应用正常播放音频。
内容的提问来源于stack exchange,提问作者LJ-dev
相关产品推荐
相关产品推荐

