一步到位:微信小程序中的录音与音频处理完全指南

817 阅读6分钟

录音

需求:长按进行录音,松开即录音完成,当录音时长达到2分钟时,自动结束录音

小程序录音APIwx.getRecorderManager() ,实例后主要用到2个事件监听:onStartonStop

注意:

  1. 录音功能,需要在微信公众平台的 用户隐私保护指引 上勾选 麦克风 才能使用
  2. !!!微信开发者工具上无法录音,只能通过 真机进行 ,迟钝的我刚开始竟然get不到,浪费了一些时间
  3. 录音会遇到 中断 情形 onInterruptionBeginonInterruptionEnd ,本篇暂时不针对此情况做处理。(监听录音因为受到系统占用而被中断开始事件。以下场景会触发此事件:微信语音聊天、微信视频聊天。此事件触发后,录音会被暂停。pause 事件在此事件后触发)
step1:添加长按控制按钮

此处使用catch防止事件冒泡,按下事件touchstart,松开事件touchend

<view
  class="tap-button"
  catchtouchstart="recordTouchStart"
  catchtouchend="recordTouchEnd"
></view>
step2:录音逻辑分析与实现
  1. 获取麦克风权限,用户已授权权限则直接下一步,未授权需授权才能继续以下流程
  2. 创建录音实例,如果当前页面已创建过实例,无需重复创建
  3. 按下按钮,调用录音实例 start 方法,start方法中按需设置自定义参数(也可以不设置,默认),设置onStart、onStop监听回调
  4. 通过 setInterval 记录录音已录制时间(小程序未看到有相关实时返回录制时间的监听事件,所以录制时间和实际录音结果时长会有些偏差)
  5. 监听 onStart 触发,设置状态
  6. 按下弹起时,结束录音 stop 方法
  7. 监听 onStop 触发,获取录音信息,并上传录音tempUrl到oss,清除setInterval,重置用到的状态
  8. 组件卸载时需要清除定时器,detached 中清除

注意:

  1. 按下弹起过快时,会出现弹起之后,才监听到录音 onStart 的回调方法,此处加一个trigger变量,在onStart 回调中判断此时是否已经是弹起状态,如果是的话,应该结束录音,出现此情况大概率录制不超过1s,提示时间过短,需重新录制

问题:

  1. setInterval记录的录制时间和实际录音时长结果有偏差,无法准确动态展示录制时间
// 长按开始
recordTouchStart () {
  this.recordStart()
},
// 长按结束
recordTouchEnd () {
  this.recordEnd()
},
// 开始逻辑
recordStart () {
  // 长按开始
  this.recorderTapping = true
  // 判断用户是否同意授权麦克风,如已经同意,调用创建录音实例
  if (!this.canRecord) {
    this.setRecordPermission()
    return
  } else if (!this.audioRecorder) {
    this.createAudioRecorder()
  }
  // 录制开始
  this.recorderTapping && this.audioRecorder.start({
    duration: this.maxRecordTime, // maxRecordTime:120000,最大可录音时长 2分钟 到时会自动停止
    format: 'mp3' // 录音格式
  })
},
// 结束逻辑
recordEnd () {
  this.recorderTapping = false
  // 判断是否录音中,onStart 被调用,即为录音中,录音中时,去手动停止录音
  if (this.isRecording) {
    this.audioRecorder.stop()
  }
},
// 监听回调 - 录音开始
onRecorderStart () {
  // 正在录音状态
  this.isRecording = true
  // 存在一种情况,按下和弹起太快了,tapend触发后,start才监听到,此时应该立刻结束掉
  if (!this.recorderTapping) {
    this.recordEnd()
    return
  }
  // 此处开启定时器,实时展示已录音时长(会和实际录音结果时长有偏差),此处只是为了动态展示,不作为最后结果获取出处
  this.startRecordTimer()
},
// 监听回调 - 录音结束:手动结束 和 设置最大录音时间时的超长录音自动结束
onRecorderStop (res) {
  this.isRecording = false
  this.clearRecordTimer()
  // 结束后会返回录制时长,针对时长做处理(毫秒)
  if (!res || !res.duration && res.duration < 1000) {
    wx.showToast({
      title: '录音时间不能小于1秒',
      icon: 'none',
      duration: 2000,
    });
    this.recordTime = 0
    return
  }
  const { tempFilePath, duration } = res
  this.recordTime = Math.round(duration / 1000).toFixed(1))
  this.recorderTimeExceed()
  // 非手动弹起,自动结束的情况
  if (this.recorderTapping) {
    this.recordEnd()
    wx.showToast({
      title: '已达录音时长上限',
      icon: 'none',
      duration: 2000,
    });
  }
  // 上传oss
  this.onUploadRecord(tempFilePath)
},
// 录制时长超过上限时,需提示,且录音会自动终止在onStop中监听到,无需手动调用stop
// 录音长按时,但录音时长超出设置的最大限度
recorderTimeExceed () {
  if (this.maxRecordTime && this.recordTime >= this.maxRecordTime) {
    this.recordTime = this.maxRecordTime
  }
},
// 上传录音
onUploadRecord (url) {
  wx.uploadFile({
    filePath: url,
    name: 'file',
    url: this.oss.endpoint,
    formData: {
      name: url,
      success_action_status: '200',
      key: 'key',
      policy: this.oss.policy,
      OSSAccessKeyId: this.oss.accessKeyId,
      signature: this.oss.signature
    },
    success: (res) => {},
    fail: (e) =>{}
  })
},
// 设置定时器,监听录音持续时长
startRecordTimer () {
  this.clearRecordTimer()
  this.recordTime = 0
  this.recordTimer = setInterval(() => {
    this.recordTime += 1
  }, 1000);
},
// 清除定时器
clearRecordTimer () {
  this.recordTimer && clearInterval(this.recordTimer)
  this.recordTimer = null
},
// 授权麦克风权限, 需要同意麦克风权限,未同意不能点
setRecordPermission () {
  wx.getSetting({
    success: (res) => {
      const auth = res.authSetting['scope.record']
      if (auth === true) { // 用户已经同意授权
        this.canRecord = true
      } else { // 首次发起授权
        wx.authorize({
          scope: 'scope.record',
          success: () => {
            this.canRecord = true
          },
          fail) => {
            this.canRecord = false
          }
        })
      }
    },
    fail: () {
      this.canRecord = false
    }
  })
},
// 创建录音实例,并设置监听方法 onStart、onStop
async createAudioRecorder () {
  if (this.audioRecorder) return
  this.audioRecorder = wx.getRecorderManager()
  this.audioRecorder.onStart(() => {
    this.onRecorderStart()
  ])
  this.audioRecorder.onStop((res) => {
    this.onRecorderStop(res)
  })
},

音频播放

坑点:

  1. 未播放音频的情况下无法获取到音频时长
  2. 二次重新播放有问题, 最简单的办法是销毁当前音频,重新创建,但是销毁后重建在开发工具上失效,真机好使
  3. 预期想获取音频时长后,将音频通过seek方式定位到开头,但是偶现失效,所以采取销毁形式

音频播放流程分析:

  1. 创建音频播放实例
  2. 设置 音频 播放链接
  3. 设置 音频 状态回调
  4. 获取音频时长
  5. 操作音频 播放、暂停、重置
  6. onCanPlay回调时,触发音频自动播放(未播放音频的情况下无法获取到音频时长)
  7. detached 销毁音频组件
// 创建实例、绑定回调监听
createAudioPlayer () {
  // 创建实例
  this.myAudio = wx.createInnerAudioContext()
  // 设置audio的资源地址
  this.myAudio.src = this.audioSrc
  
  this.myAudio.onError(() => {
    this.status = 'fail'
  })
  this.myAudio.onPlay(() => {
    this.status = 'playing'
  })
  this.myAudio.onPause(() => {
    this.status = 'pause'
  })
  this.myAudio.onEnded(() => {
    // 销毁播放器,重新创建,因为二次播放下有问题
    this.handleDestroyPlayer()
  })
  
  this.myAudio.onCanplay(() => {
    // 解决获取不到时长的问题,默认先播放,无声音,后续在onTimeUpdate回调中去获取时长时进行音频重置
    if (this.audioSrc && !this.recordTime) {
      // 只是为了获取时间的播放标志
      this.getDurationTrigger = true
      this.myAudio.volume = 0
      this.myAudio.play()
    } else {
      this.myAudio.volume = 1
    }
  })
  
  
  this.myAudio.onTimeUpdate(() => {
    if (this.myAudio) {
      if (this.getDurationTrigger) {
        this.recordTime = this.myAudio.duration // 毫秒
        // 解决可能获取时间后,开始播放时未从录音开头放的问题
        this.handleDestroyPlayer()
        this.getDurationTrigger = false
      } else {
        // 可以在播放中回调改变图标,代表播放中,并实时监听当前音频播放的进度
        console.log(this.myAudio.currentTime)
      }
    }
  })
},
handleAudioPlay () {
  if (!this.playerSrc || this.status === 'fail') return
  if (!this.myAudio) {
    this.initAudioPlayer()
  }
  if (!this.recordTime) return
  if (this.status === 'playing') {
    this.myAudio.pause()
  } else {
    this.myAudio.play()
  }
},
// 二次重新播放有问题, 最简单的办法销毁,再次点击音频的时候重新创建,但是不用二次获取音频时长了
handleDestroyPlayer ) {
  if (!this.myAudio) return
  if (this.status === 'playing') {
    this.myAudio.pause()
  }
  this.status = ''
  // 用了destroy方案,真机好使,开发工具不行
  this.myAudio.destroy()
  this.myAudio = null
},

注意:

如果一个页面中存在多个需要播放的音频,如果手动不进行控制,会有多个音频一起播放的情况,小程序不会帮你去停掉播放中的音频,再播放新的音频,需要自己控制。

解决办法:

  1. 正常通过ref即可,去调用音频播放组件的音频重置方法。
  2. 遇到了未准确获取到ref的数据(具体原因不详,小程序使用mpx形式),采取了其他办法:当播放音频时,将当前音频的 this 传递给上一级,在父级页面存储当前播放中的音频的 this ,当另一个音频点击播放,要替换当前的音频时,取出上次记录的播放中的音频实例,通过判断是否在播放中,调用组件中的重置方法,重置掉即可。