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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.23 23:36:03