React Native开发语音时钟 如何实现多个音频文件按顺序播放
问题描述
我重新接触了React Native,想通过开发原生应用进行学习,本次学习项目为语音时钟。目前时间、日期的程序已开发完成,可在屏幕上展示且每秒自动更新。我希望设置两个按钮,分别实现点击后播报当前时间、播报当前日期的功能。
我已准备好00到59的数字语音文件,以及“It is”、“and”、“o'clock”对应的语音文件,目前在音频功能开发上遇到问题:单个音频文件可以正常播放,但无法实现多个音频文件按顺序依次播放,请问大家有没有相关思路或者解决方案?
我之前的代码如下:
import React from 'react'; import type { Node } from 'react'; import { SafeAreaView, ScrollView, StatusBar, StyleSheet, Text, useColorScheme, View, Button, Alert, NativeModules, } from 'react-native'; import { Colors } from 'react-native/Libraries/NewAppScreen'; //Audio-Library import useSound from 'react-native-use-sound'; const Section = ({ children, title }): Node => { const isDarkMode = useColorScheme() === 'dark'; return ( <View style={styles.sectionContainer}> <Text style={[ styles.sectionTitle, { color: isDarkMode ? Colors.white : Colors.black, }, ]} > {title} </Text> <Text style={[ styles.sectionDescription, { color: isDarkMode ? Colors.light : Colors.dark, }, ]} > {children} </Text> </View> ); }; function Wochentag(tag = 0) { var days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; return days[tag]; } function Monat(m = 0) { var months = [ '', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December', ]; return months[m]; } function Digit(d = 0) { if (d < 10) d = '0' + d; return d; } class Uhr extends React.Component { constructor(props) { super(props); this.state = { time: Digit(new Date().getHours()) + ':' + Digit(new Date().getMinutes()), }; } componentDidMount() { this.intervalID = setInterval(() => this.tick(), 1000); } componentWillUnmount() { clearInterval(this.intervalID); } tick() { this.setState({ time: Digit(new Date().getHours()) + ':' + Digit(new Date().getMinutes()), }); } render() { return <Text style={styles.textStyle}>{this.state.time}</Text>; } } class Datum extends React.Component { constructor(props) { super(props); this.state = { datum: Digit(new Date().getDate()) + ' ' + Monat(new Date().getMonth() + 1) + ' ' + new Date().getFullYear(), wtag: Wochentag(new Date().getDay()), }; } componentDidMount() { this.intervalID = setInterval(() => this.tick(), 1000); } componentWillUnmount() { clearInterval(this.intervalID); } tick() { this.setState({ datum: Digit(new Date().getDate()) + ' ' + Monat(new Date().getMonth() + 1) + ' ' + new Date().getFullYear(), wtag: Wochentag(new Date().getDay()), }); } render() { return ( <Text style={styles.textStyle}> {this.state.wtag}, {this.state.datum} </Text> ); } } const App: () => Node = () => { const isDarkMode = useColorScheme() === 'dark'; const backgroundStyle = { backgroundColor: isDarkMode ? Colors.darker : Colors.lighter, }; //Play Audio const path = 'test.wav'; const [play, pause, stop, data] = useSound(path, { timeRate: 10, playbackRate: 1.0, volume: 1.0, }); const onPress = () => { if (!data.isPlaying) play(); else { stop(); } }; const PlayFile = <Button title={data.isPlaying ? 'Pause' : 'Play'} onPress={() => onPress()} />; return ( <SafeAreaView style={backgroundStyle}> <StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} /> <ScrollView contentInsetAdjustmentBehavior="automatic" style={backgroundStyle}> <View style={{ backgroundColor: isDarkMode ? Colors.black : Colors.white, }} > <Section title="Aktuelle Uhrzeit ist: "> <Uhr /> Uhr </Section> <Section title="Heute ist: "> <Datum /> </Section> <Section title=""></Section> <Button title="Uhrzeit ansagen" onPress={() => Alert.alert('Noch wird die Uhrzeit nicht angesagt. das kommt aber sehr bald....')} /> <Button title="Datum ansagen" onPress={() => Alert.alert('Noch wird das Datum nicht angesagt. das kommt aber sehr bald....')} /> {PlayFile} </View> </ScrollView> </SafeAreaView> ); }; const styles = StyleSheet.create({ sectionContainer: { marginTop: 32, paddingHorizontal: 24, }, sectionTitle: { fontSize: 24, fontWeight: '600', }, sectionDescription: { marginTop: 8, fontSize: 18, fontWeight: '400', }, highlight: { fontWeight: '700', }, }); export default App;
更新
有没有人知道如何参考相关案例解决我的问题?
新更新
以下代码可以正常运行,但效果不够理想,有没有人有更优的解决方案?
import Sound from 'react-native-sound'; function Digit(d=0) { if(d < 10) d = '0' + d; return d; } function AudioClock(datei) { Sound.setCategory('Playback'); var audioFile = new Sound(datei, Sound.MAIN_BUNDLE, (error) => { if (error) { console.log('failed to load the sound', error); return; } // loaded successfully console.log('duration in seconds: ' + audioFile.getDuration() + 'number of channels: ' + audioFile.getNumberOfChannels()); audioFile.setVolume(1); audioFile.setPan(1); audioFile.play((success) => { if (success) { console.log('successfully finished playing'); } else { console.log('playback failed due to audio decoding errors'); } }); }); ... const SayTime = () => { setTimeout( () => AudioClock('it_is.wav'), 10); // it is setTimeout( () => AudioClock('uhr_' + Digit(new Date().getHours()) +'.wav'), 900); // hours setTimeout(() => AudioClock('and.wav'), 1400); // and setTimeout( () => AudioClock('uhr_' + Digit(new Date().getMinutes()) + '.wav'), 2000); // minutes setTimeout( () => AudioClock('o_clock.wav'), 3100); // o'clock }; ... <Button title="Say time" onPress={SayTime} />
解决方案
你当前用固定时长setTimeout调度的方案问题很明显,每个音频实际时长不固定、不同设备性能差异都会导致音频衔接出现重叠或者空白过长的问题,优化的核心是利用react-native-sound播放完成的回调,前一个音频播放结束后再触发下一个,用Promise队列处理就可以实现稳定的顺序播放。
实现代码
import Sound from 'react-native-sound'; // 全局音频配置初始化,只需要执行一次即可 Sound.setCategory('Playback'); // 格式化数字的工具函数 function Digit(d=0) { if(d < 10) d = '0' + d; return d; } // 封装单个音频播放为Promise,播放完成后resolve,失败则reject const playSingleAudio = (fileName) => { return new Promise((resolve, reject) => { const audioInstance = new Sound(fileName, Sound.MAIN_BUNDLE, (loadError) => { if (loadError) { console.log(`音频${fileName}加载失败:`, loadError); reject(loadError); return; } // 播放音频 audioInstance.play((playSuccess) => { audioInstance.release(); // 播放结束后立即释放资源,避免内存泄漏 if (playSuccess) { resolve(); } else { const playError = new Error(`音频${fileName}播放失败`); console.log(playError.message); reject(playError); } }); }); }); }; // 按顺序播放音频队列 const playAudioQueue = async (audioFileList) => { for (const file of audioFileList) { try { await playSingleAudio(file); } catch (error) { console.error('音频播放队列中断:', error); // 可根据需求调整逻辑,比如break中断播放,或者continue跳过当前失败音频继续播放 break; } } }; // 播报时间的实现 const SayTime = async () => { const now = new Date(); const hours = Digit(now.getHours()); const minutes = Digit(now.getMinutes()); // 按播报顺序整理音频队列 const timePlayList = [ 'it_is.wav', `uhr_${hours}.wav`, 'and.wav', `uhr_${minutes}.wav`, 'o_clock.wav' ]; await playAudioQueue(timePlayList); }; // 播报日期的实现同理,整理对应音频队列即可 const SayDate = async () => { const now = new Date(); // 按你的播报需求拼接对应的音频文件队列,示例: // const datePlayList = [ // 'today_is.wav', // `${Wochentag(now.getDay())}.wav`, // `${Monat(now.getMonth()+1)}.wav`, // `${Digit(now.getDate())}.wav`, // `${now.getFullYear()}.wav` // ]; // await playAudioQueue(datePlayList); };
方案优势
- 完全跟随音频实际播放进度调度,不会出现音频重叠或者衔接空白过长的问题
- 错误处理灵活,可自行调整单个音频播放失败后的逻辑
- 代码复用性高,后续新增其他播报功能只需要整理对应音频队列即可,不需要重复编写播放逻辑
内容的提问来源于stack exchange,提问作者vbprofi
相关产品推荐
相关产品推荐

