uni-app 音频处理

核心知识点

1. 音频基础概念

  • 音频格式:MP3、WAV、AAC、OGG、FLAC 等
  • 音频参数:采样率、位深度、声道数、码率
  • 音频文件大小:与参数设置的关系
  • 音频质量:不同格式和参数的音质差异

2. 音频录制

  • 录制 API:uni.getRecorderManager()
  • 录制配置:格式、采样率、码率等
  • 录制控制:开始、暂停、恢复、停止
  • 录制状态监听:开始、暂停、停止、错误等
  • 录音权限:需要获取麦克风权限

3. 音频播放

  • 播放 API:uni.createInnerAudioContext()
  • 播放控制:播放、暂停、停止、快进、快退
  • 音量控制:系统音量和播放器音量
  • 播放状态监听:播放中、暂停、停止、结束、错误等
  • 音频焦点:处理音频中断和恢复

4. 音频编辑

  • 音频裁剪:截取音频片段
  • 音频合并:将多个音频文件合并为一个
  • 音频转换:格式转换、参数调整
  • 音频特效:添加音效、混响、均衡器等
  • 音频分析:波形分析、频谱分析

5. 音频优化

  • 音频加载优化:预加载、缓存策略
  • 音频播放优化:低延迟播放、无缝播放
  • 内存管理:及时释放音频资源
  • 电池优化:减少后台音频消耗
  • 网络优化:音频流传输优化

6. 跨平台音频处理

  • 平台差异:不同平台对音频格式和 API 的支持
  • 条件编译:为不同平台提供不同的音频处理方案
  • 原生能力:使用平台原生的音频处理 API
  • 第三方库:使用跨平台音频处理库

实用案例

案例 1:语音备忘录应用

需求分析

  • 支持录制语音备忘录
  • 支持播放、暂停、删除录音
  • 显示录音时长和文件大小
  • 支持录音列表管理

实现方案

  1. 音频录制

    <template>
      <view class="record-section">
        <button 
          @click="toggleRecord" 
          :type="isRecording ? 'warn' : 'primary'"
          class="record-btn"
        >
          {{ isRecording ? '停止录音' : '开始录音' }}
        </button>
        <text class="record-duration" v-if="isRecording">
          录音时长: {{ formatTime(duration) }}
        </text>
        <view class="录音列表">
          <view v-for="(item, index) in recordings" :key="index" class="recording-item">
            <view class="recording-info">
              <text class="recording-name">{{ item.name }}</text>
              <text class="recording-duration">{{ formatTime(item.duration) }}</text>
            </view>
            <view class="recording-actions">
              <button @click="playRecording(item)" size="mini" type="primary">播放</button>
              <button @click="deleteRecording(index)" size="mini" type="warn">删除</button>
            </view>
          </view>
        </view>
      </view>
    </template>
    
    <script>
    export default {
      data() {
        return {
          recorderManager: null,
          innerAudioContext: null,
          isRecording: false,
          isPlaying: false,
          duration: 0,
          recordings: [],
          timer: null
        }
      },
      onLoad() {
        this.initRecorder()
        this.initPlayer()
        this.loadRecordings()
      },
      onUnload() {
        // 释放资源
        if (this.recorderManager) {
          this.recorderManager.stop()
        }
        if (this.innerAudioContext) {
          this.innerAudioContext.stop()
          this.innerAudioContext.destroy()
        }
        if (this.timer) {
          clearInterval(this.timer)
        }
      },
      methods: {
        initRecorder() {
          this.recorderManager = uni.getRecorderManager()
          
          this.recorderManager.onStart(() => {
            console.log('开始录音')
            this.isRecording = true
            this.duration = 0
            this.timer = setInterval(() => {
              this.duration++
            }, 1000)
          })
          
          this.recorderManager.onStop((res) => {
            console.log('停止录音', res)
            this.isRecording = false
            if (this.timer) {
              clearInterval(this.timer)
              this.timer = null
            }
            
            const recording = {
              name: `录音 ${new Date().toLocaleString()}`,
              path: res.tempFilePath,
              duration: this.duration,
              size: res.fileSize
            }
            
            this.recordings.push(recording)
            this.saveRecordings()
          })
          
          this.recorderManager.onError((err) => {
            console.error('录音错误', err)
            this.isRecording = false
            if (this.timer) {
              clearInterval(this.timer)
              this.timer = null
            }
          })
        },
        
        initPlayer() {
          this.innerAudioContext = uni.createInnerAudioContext()
          this.innerAudioContext.onPlay(() => {
            console.log('开始播放')
            this.isPlaying = true
          })
          this.innerAudioContext.onPause(() => {
            console.log('暂停播放')
            this.isPlaying = false
          })
          this.innerAudioContext.onStop(() => {
            console.log('停止播放')
            this.isPlaying = false
          })
          this.innerAudioContext.onEnded(() => {
            console.log('播放结束')
            this.isPlaying = false
          })
          this.innerAudioContext.onError((err) => {
            console.error('播放错误', err)
            this.isPlaying = false
          })
        },
        
        toggleRecord() {
          if (this.isRecording) {
            this.recorderManager.stop()
          } else {
            // 检查权限
            uni.getSetting({ 
              success: (res) => {
                if (!res.authSetting['scope.record']) {
                  uni.authorize({ 
                    scope: 'scope.record',
                    success: () => {
                      this.startRecording()
                    },
                    fail: () => {
                      uni.showToast({ 
                        title: '需要麦克风权限才能录音',
                        icon: 'none'
                      })
                    }
                  })
                } else {
                  this.startRecording()
                }
              }
            })
          }
        },
        
        startRecording() {
          this.recorderManager.start({
            format: 'mp3',
            sampleRate: 44100,
            numberOfChannels: 2,
            encodeBitRate: 128000
          })
        },
        
        playRecording(recording) {
          if (this.isPlaying) {
            this.innerAudioContext.stop()
          }
          
          this.innerAudioContext.src = recording.path
          this.innerAudioContext.play()
        },
        
        deleteRecording(index) {
          uni.showModal({
            title: '确认删除',
            content: '确定要删除这个录音吗?',
            success: (res) => {
              if (res.confirm) {
                this.recordings.splice(index, 1)
                this.saveRecordings()
              }
            }
          })
        },
        
        saveRecordings() {
          uni.setStorageSync('recordings', this.recordings)
        },
        
        loadRecordings() {
          const recordings = uni.getStorageSync('recordings')
          if (recordings) {
            this.recordings = recordings
          }
        },
        
        formatTime(seconds) {
          const minutes = Math.floor(seconds / 60)
          const remainingSeconds = seconds % 60
          return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`
        }
      }
    }
    </script>
  2. 录音列表管理

    • 使用本地存储保存录音列表
    • 支持录音重命名和分类
    • 实现录音搜索和排序功能

案例 2:音频播放器应用

需求分析

  • 支持播放本地和网络音频文件
  • 支持音频控制:播放、暂停、上一曲、下一曲
  • 显示音频播放进度和时长
  • 支持音频列表管理和随机播放
  • 支持音频均衡器和音效设置

实现方案

  1. 音频播放

    <template>
      <view class="player-section">
        <view class="player-info">
          <text class="audio-title">{{ currentAudio.title }}</text>
          <text class="audio-artist">{{ currentAudio.artist }}</text>
        </view>
        
        <view class="progress-section">
          <text class="time">{{ formatTime(currentTime) }}</text>
          <view class="progress-bar" @click="seek">
            <view class="progress-track"></view>
            <view class="progress-fill" :style="{ width: `${progress}%` }"></view>
            <view class="progress-thumb" :style="{ left: `${progress}%` }"></view>
          </view>
          <text class="time">{{ formatTime(duration) }}</text>
        </view>
        
        <view class="control-section">
          <button @click="prev" class="control-btn">上一曲</button>
          <button @click="togglePlay" class="control-btn primary">
            {{ isPlaying ? '暂停' : '播放' }}
          </button>
          <button @click="next" class="control-btn">下一曲</button>
        </view>
        
        <view class="eq-section">
          <text class="eq-title">均衡器</text>
          <view class="eq-presets">
            <button 
              v-for="(preset, index) in eqPresets" 
              :key="index"
              @click="setEQPreset(preset)"
              :class="{ active: currentPreset === preset }"
              class="eq-preset-btn"
            >
              {{ preset }}
            </button>
          </view>
        </view>
      </view>
    </template>
    
    <script>
    export default {
      data() {
        return {
          innerAudioContext: null,
          audioList: [
            {
              id: 1,
              title: '歌曲 1',
              artist: '艺术家 1',
              url: 'https://example.com/song1.mp3'
            },
            {
              id: 2,
              title: '歌曲 2',
              artist: '艺术家 2',
              url: 'https://example.com/song2.mp3'
            },
            {
              id: 3,
              title: '歌曲 3',
              artist: '艺术家 3',
              url: 'https://example.com/song3.mp3'
            }
          ],
          currentIndex: 0,
          isPlaying: false,
          currentTime: 0,
          duration: 0,
          progress: 0,
          eqPresets: ['正常', '摇滚', '流行', '爵士', '古典'],
          currentPreset: '正常'
        }
      },
      computed: {
        currentAudio() {
          return this.audioList[this.currentIndex]
        }
      },
      onLoad() {
        this.initPlayer()
        this.loadAudio(this.currentAudio.url)
      },
      onUnload() {
        if (this.innerAudioContext) {
          this.innerAudioContext.stop()
          this.innerAudioContext.destroy()
        }
      },
      methods: {
        initPlayer() {
          this.innerAudioContext = uni.createInnerAudioContext()
          
          this.innerAudioContext.onPlay(() => {
            console.log('开始播放')
            this.isPlaying = true
          })
          
          this.innerAudioContext.onPause(() => {
            console.log('暂停播放')
            this.isPlaying = false
          })
          
          this.innerAudioContext.onStop(() => {
            console.log('停止播放')
            this.isPlaying = false
          })
          
          this.innerAudioContext.onEnded(() => {
            console.log('播放结束')
            this.isPlaying = false
            this.next()
          })
          
          this.innerAudioContext.onTimeUpdate(() => {
            this.currentTime = this.innerAudioContext.currentTime
            this.duration = this.innerAudioContext.duration || 0
            this.progress = (this.currentTime / this.duration) * 100
          })
          
          this.innerAudioContext.onError((err) => {
            console.error('播放错误', err)
            this.isPlaying = false
          })
        },
        
        loadAudio(url) {
          this.innerAudioContext.src = url
          this.innerAudioContext.play()
        },
        
        togglePlay() {
          if (this.isPlaying) {
            this.innerAudioContext.pause()
          } else {
            this.innerAudioContext.play()
          }
        },
        
        prev() {
          this.currentIndex = (this.currentIndex - 1 + this.audioList.length) % this.audioList.length
          this.loadAudio(this.currentAudio.url)
        },
        
        next() {
          this.currentIndex = (this.currentIndex + 1) % this.audioList.length
          this.loadAudio(this.currentAudio.url)
        },
        
        seek(e) {
          const { clientX, target } = e
          const rect = target.getBoundingClientRect()
          const percentage = (clientX - rect.left) / rect.width
          const seekTime = percentage * this.duration
          this.innerAudioContext.seek(seekTime)
        },
        
        setEQPreset(preset) {
          this.currentPreset = preset
          // 这里可以实现均衡器设置
          console.log('设置均衡器预设:', preset)
        },
        
        formatTime(seconds) {
          if (!seconds) return '00:00'
          const minutes = Math.floor(seconds / 60)
          const remainingSeconds = Math.floor(seconds % 60)
          return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`
        }
      }
    }
    </script>
  2. 音频列表管理

    • 支持本地音频扫描和网络音频添加
    • 实现音频分类和播放列表管理
    • 支持音频搜索和排序功能
  3. 音频优化

    • 实现音频预加载,减少播放延迟
    • 使用音频缓存,减少网络请求
    • 优化后台播放,提升用户体验

案例 3:语音识别应用

需求分析

  • 支持实时语音识别
  • 支持语音转文本
  • 支持文本转语音
  • 支持语音命令控制

实现方案

  1. 语音识别
    <template>
      <view class="speech-section">
        <button 
          @click="toggleSpeechRecognition" 
          :type="isRecognizing ? 'warn' : 'primary'"
          class="speech-btn"
        >
          {{ isRecognizing ? '停止识别' : '开始识别' }}
        </button>
        <view class="result-section">
          <text class="result-title">识别结果</text>
          <text class="result-text">{{ recognitionResult }}</text>
        </view>
        <button @click="textToSpeech" type="primary" class="tts-btn">
          文本转语音
        </button>
      </view>
    </template>
    
    <script>
    export default {
      data() {
        return {
          isRecognizing: false,
          recognitionResult: '',
          ttsContext: null
        }
      },
      onLoad() {
        this.initTTS()
      },
      methods: {
        toggleSpeechRecognition() {
          if (this.isRecognizing) {
            this.stopRecognition()
          } else {
            this.startRecognition()
          }
        },
        
        startRecognition() {
          // 检查权限
          uni.getSetting({ 
            success: (res) => {
              if (!res.authSetting['scope.record']) {
                uni.authorize({ 
                  scope: 'scope.record',
                  success: () => {
                    this.doRecognition()
                  },
                  fail: () => {
                    uni.showToast({ 
                      title: '需要麦克风权限才能识别',
                      icon: 'none'
                    })
                  }
                })
              } else {
                this.doRecognition()
              }
            }
          })
        },
        
        doRecognition() {
          this.isRecognizing = true
          this.recognitionResult = '正在识别...'
          
          // 这里使用模拟数据,实际项目中需要调用语音识别 API
          setTimeout(() => {
            this.recognitionResult = '这是一段语音识别结果'
            this.isRecognizing = false
          }, 3000)
        },
        
        stopRecognition() {
          this.isRecognizing = false
          this.recognitionResult = '识别已停止'
        },
        
        initTTS() {
          this.ttsContext = uni.createInnerAudioContext()
        },
        
        textToSpeech() {
          if (!this.recognitionResult) {
            uni.showToast({ 
              title: '请先进行语音识别',
              icon: 'none'
            })
            return
          }
          
          // 这里使用模拟数据,实际项目中需要调用文本转语音 API
          uni.showToast({ 
            title: '正在播放语音',
            icon: 'none'
          })
          
          // 模拟播放
          setTimeout(() => {
            uni.showToast({ 
              title: '语音播放完成',
              icon: 'none'
            })
          }, 2000)
        }
      }
    }
    </script>

学习目标

  1. 掌握 uni-app 中音频处理的核心 API 和方法
  2. 学会实现音频录制、播放、编辑等功能
  3. 掌握音频优化的技巧和方法
  4. 了解跨平台音频处理的差异和解决方案
  5. 能够开发包含音频功能的完整应用
  6. 提升应用中音频的用户体验

总结

音频处理是 uni-app 应用开发中的重要组成部分,通过合理使用音频 API、优化音频参数、实现高效的音频管理,可以显著提升应用的用户体验。在实际开发中,开发者应该根据具体场景选择合适的音频格式和参数,平衡音质和文件大小,为用户提供最佳的音频体验。

同时,开发者还需要注意跨平台音频处理的差异,使用条件编译或平台特定的 API 来处理不同平台的音频问题,确保应用在所有平台上都能正常运行音频功能。对于需要高质量音频处理的应用,还可以考虑使用第三方音频处理库或云服务,提升音频处理能力。

« 上一篇 uni-app 视频优化 下一篇 » uni-app 地图高级功能