ps:第二次播放不用请求接口版本,拿取缓存数据

页面:

<audioPlay ref="audioPlay" @listenOver="listenOver"></audioPlay>

listen(item) {
  if(this.isListen) return;
  this.isListen = true;
  this.$refs.audioPlay.listen(item)
}

listenOver(){
    this.isListen = false;
},

封装的组件:


<template>
  <view>
    <!-- 无UI元素 -->
  </view>
</template>

<script>
export default {
  data() {
    return {
      innerAudioContext: null,
      audioList: [],
      currentIndex: 0,
      TTS_API: '/otherApi/api/tts',
      MALE_VOICE_ID: "Andrew Chipper",
      FEMALE_VOICE_ID: "Claribel Dervla",
      isPlaying: false,
      dingAudio: null,
      currentItem: null,
      audioCache: {} // 音频缓存
    }
  },
  created() {
    this.initAudio();
    this.dingAudio = uni.createInnerAudioContext();
    this.dingAudio.src = '/static/ding.mp3';
  },
  methods: {
    initAudio() {
      if (this.innerAudioContext) {
        this.innerAudioContext.destroy();
      }
      
      this.innerAudioContext = uni.createInnerAudioContext();
      this.innerAudioContext.onEnded(() => {
        this.playNext();
      });
      this.innerAudioContext.onError((res) => {
        console.error('音频播放错误:', res);
        this.handlePlayError();
      });
    },
    
    async listen(item) {
        console.log(item.topicContentJson.listenContent)
      try {
        if (this.isPlaying) {
          this.stopPlayback();
          await this.delay(200);
        }
        
        this.currentItem = item;
        uni.showLoading({
          title: "音频加载中"
        });
        
        const speechParts = this.processTextToSpeech(item.topicContentJson.listenContent);
        this.audioList = await this.prepareAudioList(speechParts);
        this.currentIndex = 0;
        this.isPlaying = true;
        
        await this.startPlayback();
      } catch (error) {
        console.error('播放初始化失败:', error);
        this.handlePlayError();
        uni.hideLoading();
      } finally {
        uni.hideLoading();
      }
    },
    
    async prepareAudioList(speechParts) {
      const result = [];
      for (const part of speechParts) {
        const cacheKey = `${part.text}-${part.isMale}`;
        if (!this.audioCache[cacheKey]) {
          // 使用uni.request获取音频
          const audioUrl = await this.getTTSAudio(part.text, part.isMale);
          this.audioCache[cacheKey] = audioUrl;
        }
        result.push({
          url: this.audioCache[cacheKey],
          text: part.text
        });
      }
      return result;
    },
    
    // 使用uni.request获取TTS音频
    getTTSAudio(text, isMale) {
      return new Promise((resolve, reject) => {
        uni.request({
          url: this.TTS_API,
          method: 'GET',
          data: {
            text: text,
            speaker_id: isMale ? this.MALE_VOICE_ID : this.FEMALE_VOICE_ID,
            language_id: "en",
            style_wav: ""
          },
          responseType: 'arraybuffer', // 重要:获取二进制数据
          success: (res) => {
            if (res.statusCode === 200) {
              // H5环境处理
              const blob = new Blob([res.data], { type: 'audio/mp3' });
              const audioUrl = URL.createObjectURL(blob);
              resolve(audioUrl);
            } else {
              reject(new Error(`TTS请求失败,状态码: ${res.statusCode}`));
            }
          },
          fail: (err) => {
            reject(err);
          }
        });
      });
    },
    
    async startPlayback() {
      try {
        await this.playDingSound();
        await this.delay(100);
        await this.playCurrentAudio();
        

      } catch (error) {
        console.error('播放流程错误:', error);
        throw error;
      }
    },
    
    playCurrentAudio() {
      return new Promise((resolve, reject) => {
        if (this.currentIndex >= this.audioList.length || !this.isPlaying) {
          this.isPlaying = false;
          resolve();
          return;
        }
        
        const currentAudio = this.audioList[this.currentIndex];
        // console.log('播放音频:', currentAudio.text);
        
        this.innerAudioContext.src = currentAudio.url;
        this.innerAudioContext.play();
        
        this.innerAudioContext.onEnded(() => {
              if (this.currentIndex >= this.audioList.length || !this.isPlaying){
                  console.log('音频自然结束');
                  this.$emit("listenOver");
              }
        });
        const playTimer = setTimeout(() => {
          reject(new Error('音频播放超时'));
        }, 10000);
        
        const playSuccess = () => {
          clearTimeout(playTimer);
          resolve();
        };
        
        this.innerAudioContext.onPlay(playSuccess);
        this.innerAudioContext.onError((err) => {
          clearTimeout(playTimer);
          reject(err);
        });
      });
    },
    
    async playNext() {
      this.currentIndex++;
      if (this.currentIndex < this.audioList.length) {
        await this.delay(400);
        await this.playCurrentAudio();
      } else {
        this.isPlaying = false;
      }
    },
    
    stopPlayback() {
      this.isPlaying = false;
      if (this.innerAudioContext) {
        this.innerAudioContext.stop();
      }
      this.audioList = [];
      this.currentIndex = 0;
    },
    
    playDingSound() {
      return new Promise((resolve) => {
        this.dingAudio.stop();
        this.dingAudio.play();
        this.dingAudio.onEnded(resolve);
        this.dingAudio.onError(resolve);
      });
    },
    
    handlePlayError() {
      this.isPlaying = false;
      uni.showToast({
        title: '音频播放失败,请稍后再试',
        icon: 'none'
      });
    },
    
    delay(ms) {
      return new Promise(resolve => setTimeout(resolve, ms));
    },
    
    // 文本处理函数保持不变
    processTextToSpeech(text) {
        const lines = text.split(/\n|<br>|\/br>/)
            .map(line => line.trim())
            .filter(line => line.length > 0);
                
        // 判断是否为对话模式(检测是否有角色标记)
        const isDialogue = lines.some(line =>
            /^(M|m|W|w|男|女|Male|Female)[::]/.test(line)
        );
                
        if (!isDialogue) {
            return [{ text: lines.join(' '), isMale: false }];
        }
            
        const normalizedText = text.replace(/<br>|\/br>/g, '\n');
        // 2. 终极正则表达式:精确匹配角色和完整内容
        const dialogueRegex = /([MW男女])\s*[::]\s*([\s\S]*?)(?=\n?[MW男女]\s*[::]|$)/gi;
        const parts = [];
        let lastIndex = 0;
        let currentSpeaker = null;
        // 3. 遍历所有对话段落
        while (true) {
            const match = dialogueRegex.exec(normalizedText);
            if (!match) break;
            const [fullMatch, role, content] = match;
            const isMale = /^[M男]/i.test(role);
            currentSpeaker = isMale ? 'M' : 'W';
            // 4. 处理匹配前的文本(如果有)
            if (match.index > lastIndex) {
                const betweenText = normalizedText.slice(lastIndex, match.index).trim();
                if (betweenText && currentSpeaker) {
                    parts.push(...this.splitCompleteSentences(betweenText, isMale));
                }
            }
            // 5. 处理当前内容(确保完整保留)
            if (content.trim()) {
                parts.push(...this.splitCompleteSentences(content.trim(), isMale));
            }
            lastIndex = dialogueRegex.lastIndex;
        }
        // 6. 处理最后一段文本
        if (lastIndex < normalizedText.length) {
            const remainingText = normalizedText.slice(lastIndex).trim();
            if (remainingText && currentSpeaker) {
                parts.push(...this.splitCompleteSentences(remainingText, currentSpeaker === 'M'));
            }
        }
        return parts;
    },
    splitCompleteSentences(text, isMale) {
      const sentences = text.match(/[^.!?]+[.!?]*/g) || [text];
      return sentences
        .map(s => s.trim())
        .filter(s => s.length > 0)
        .map(sentence => ({
          text: sentence,
          isMale
        }));
    }
  },
  
  beforeDestroy() {
    // 释放音频资源
    if (this.innerAudioContext) {
      this.innerAudioContext.destroy();
    }
    if (this.dingAudio) {
      this.dingAudio.destroy();
    }
    // 释放Blob URL
    Object.values(this.audioCache).forEach(url => {
      URL.revokeObjectURL(url);
    });
  }
}
</script>

更多推荐