微信小程序实现长按录音,点击播放等功能,CSS实现语音录制动画效果

有一个需求需要在微信小程序上实现一个长按时进行语音录制,录制时间最大为60秒,录制完成后,可点击播放,播放时再次点击停止播放,可以反复录制,新录制的语音把之前的语音覆盖掉,也可以主动长按删除

// index.js
const recorderManager = wx.getRecorderManager()
const innerAudioContext = wx.createInnerAudioContext()
let recordingTimerInterval = null // 录音时长计时器
let countdownTimerInterval = null // 倒计时计时器
let playbackCountdownInterval = null // 播放倒计时计时器

Page({
/**
   * 页面的初始数据
   */
  data: {
    // 语音输入部分
    inputType: 'input',
    count: null, // 录制倒计时
    longPress: '1', // 1显示 按住说话 2显示 说话中
    delShow: false, // 删除提示框显示隐藏
    time: 0, // 录音时长
    recordedDuration: 0, // 已录制音频的时长
    duration: 60000, // 录音最大值ms 60000/1分钟
    tempFilePath: '', //音频路径
    playStatus: 0, //录音播放状态 0:未播放 1:正在播放
    currentTime: 0, // 当前播放进度(秒)
    remain: 0, // 当前剩余时长(秒) = duration - currentTime
    warningShown: false, // 是否已显示50秒提示
    minDuration: 2, // 录音最小时长秒数
    animationArray: Array.from({ length: 15 }, (_, index) => {
      // length 这个名字就不再需要,因为我们已经在这里写死了 15
      const centerIndex = Math.floor((15 - 1) / 2) // 7
      const distance = Math.abs(index - centerIndex)
      // 中心延迟为 0,向外越来越大
      const delay = distance * 0.2
      return { delay }
    })
  },
  /**
   * 开始录音倒计时
   * @param {number} val - 倒计时秒数
   */
  startCountdown(val) {
    this.setData({
      count: Number(val)
    })
    countdownTimerInterval = setInterval(() => {
      if (this.data.count > 0) {
        this.setData({
          count: this.data.count - 1
        })
      } else {
        this.setData({
          longPress: '1'
        })
        clearInterval(countdownTimerInterval)
        countdownTimerInterval = null
      }
    }, 1000)
  },
    /**
   * 开始录音时长计时
   */
  startRecordingTimer() {
    if (recordingTimerInterval) return // 防止重复启动计时器
    recordingTimerInterval = setInterval(() => {
      this.setData({
        time: this.data.time + 1
      })

      // 当录音时长达到50秒且未显示提示时,显示提示
      if (this.data.time === 50 && !this.data.warningShown) {
        wx.showToast({
          title: '录音即将结束',
          icon: 'none',
          duration: 2000
        })
        this.setData({
          warningShown: true
        })
      }

      // 如果录音时长达到最大值,自动停止录音
      if (this.data.time >= this.data.duration / 1000) {
        wx.showToast({
          title: '录音已达到最大时长',
          icon: 'none'
        })
        this.touchendBtn()
      }
    }, 1000)
  },
  /**
   * 停止录音时长计时
   * @param {string} newTempFilePath - 新录音的文件路径
   */
  stopRecordingTimer(newTempFilePath) {
    if (recordingTimerInterval) {
      clearInterval(recordingTimerInterval)
      recordingTimerInterval = null
    }

    const duration = this.data.time

    if (duration >= this.data.minDuration) {
      this.setData(
        {
          recordedDuration: duration,
          tempFilePath: newTempFilePath
        },
        () => {
          console.log('录音已停止,时长:', this.data.recordedDuration, '秒')
        }
      )
    } else {
      // 录音时长过短,提示用户
      wx.showToast({
        title: '录音时间太短',
        icon: 'none'
      })

      // 不覆盖之前的 tempFilePath,保留旧的录音
      // 仅重置 time
      this.setData(
        {
          time: 0
        },
        () => {
          console.log('录音时间太短,不保存此次录音。')
        }
      )
    }
  },
  /**
   * 开始播放倒计时
   * @param {number} val - 播放倒计时秒数
   */
  startPlaybackCountdown(val) {
    // 先停止可能存在的旧计时器
    if (playbackCountdownInterval) {
      clearInterval(playbackCountdownInterval)
      playbackCountdownInterval = null
    }

    this.setData({
      count: Number(val)
    })

    playbackCountdownInterval = setInterval(() => {
      if (this.data.count > 0) {
        this.setData({
          count: this.data.count - 1
        })
      } else {
        // 播放结束
        this.setData({
          playStatus: 0,
          count: null
        })
        innerAudioContext.stop()
        clearInterval(playbackCountdownInterval)
        playbackCountdownInterval = null
      }
    }, 1000)
  },
  /**
   * 停止播放倒计时
   */
  stopPlaybackCountdown() {
    if (playbackCountdownInterval) {
      clearInterval(playbackCountdownInterval)
      playbackCountdownInterval = null
    }
    this.setData({
      count: null
    })
  },
  /**
   * 清除所有计时器
   */
  clearAllTimers() {
    if (recordingTimerInterval) {
      clearInterval(recordingTimerInterval)
      recordingTimerInterval = null
    }
    if (countdownTimerInterval) {
      clearInterval(countdownTimerInterval)
      countdownTimerInterval = null
    }
    if (playbackCountdownInterval) {
      clearInterval(playbackCountdownInterval)
      playbackCountdownInterval = null
    }
  },
  /**
   * 重置录音状态
   */
  resetRecordingState() {
    this.setData({
      longPress: '1',
      time: 0,
      recordedDuration: 0,
      count: null,
      warningShown: false // 重置警告提示
    })
    this.stopRecordingTimer()
    this.stopCountdownTimer()
  },
  /**
   * 处理输入类型变化
   * @param {object} e - 事件对象
   */
  handleChangeInputType(e) {
    const { type } = e.currentTarget.dataset
    this.setData({
      inputType: type
    })
  },
  /**
   * 检查录音权限
   */
  checkRecordPermission() {
    wx.getSetting({
      success: res => {
        if (!res.authSetting['scope.record']) {
          // 没有录音权限,尝试授权
          wx.authorize({
            scope: 'scope.record',
            success: () => {
              // 授权成功,可以开始录音
              this.startRecording()
            },
            fail: () => {
              // 授权失败,提示用户前往设置授权
              wx.showModal({
                title: '授权提示',
                content: '录音权限未授权,请前往设置授权',
                success: res => {
                  if (res.confirm) {
                    wx.openSetting()
                  }
                }
              })
            }
          })
        } else {
          // 已经授权,可以开始录音
          this.startRecording()
        }
      },
      fail: () => {
        // 获取设置失败,提示用户
        wx.showToast({
          title: '获取权限失败,请重试',
          icon: 'none'
        })
      }
    })
  },
  /**
   * 开始录音的封装函数
   */
  startRecording() {
    this.setData({
      longPress: '2',
      time: 0, // 在开始录音前重置 time
      warningShown: false // 重置警告提示
    })
    this.startCountdown(this.data.duration / 1000) // 录音倒计时60秒
    //recorderManager.stop() // 确保之前的录音已停止
    this.startRecordingTimer()
    const options = {
      duration: this.data.duration * 1000, // 指定录音的时长,单位 ms
      sampleRate: 16000, // 采样率
      numberOfChannels: 1, // 录音通道数
      encodeBitRate: 96000, // 编码码率
      format: 'mp3', // 音频格式,有效值 aac/mp3
      frameSize: 10 // 指定帧大小,单位 KB
    }
    recorderManager.start(options)
  },
  /**
   * 长按录音事件
   */
  longpressBtn() {
    this.checkRecordPermission()
  },
  /**
   * 长按松开录音事件
   */
  touchendBtn() {
    this.setData({
      longPress: '1'
    })
    recorderManager.stop()
    this.stopCountdownTimer()
  },
  /**
   * 停止倒计时计时器
   */
  stopCountdownTimer() {
    if (countdownTimerInterval) {
      clearInterval(countdownTimerInterval)
      countdownTimerInterval = null
    }
    this.setData({
      count: null
    })
  },
  /**
   * 播放录音
   */
  playBtn() {
    if (!this.data.tempFilePath) {
      wx.showToast({
        title: '没有录音文件',
        icon: 'none'
      })
      return
    }

    // 如果已经在播放,就先停止
    if (this.data.playStatus === 1) {
      innerAudioContext.stop()
      // 重置状态
      this.setData({
        playStatus: 0,
        currentTime: 0,
        remain: 0
      })
    } else {
      // 重新开始播放
      console.log('开始播放', this.data.tempFilePath)
      innerAudioContext.src = this.data.tempFilePath
      // 在 iOS 下,即使系统静音,也能播放音频
      innerAudioContext.obeyMuteSwitch = false
      // 播放
      innerAudioContext.play()

      // playStatus 会在 onPlay 中置为 1
      // 如果想在点击之后就立即把 playStatus 置为 1 也行
    }
  },
  /**
   * 生命周期函数--监听页面加载
   */
  onLoad(options) {
    // 绑定录音停止事件
    recorderManager.onStop(res => {
      // 将新录音的文件路径传递给 stopRecordingTimer
      this.stopRecordingTimer(res.tempFilePath)
      console.log('录音已停止,文件路径:', res.tempFilePath)
      console.log('录音时长:', this.data.recordedDuration, '秒')
    })

    // 绑定录音开始事件
    recorderManager.onStart(res => {
      console.log('录音开始', res)
    })

    // 绑定录音错误事件
    recorderManager.onError(err => {
      console.error('录音错误:', err)
      wx.showToast({
        title: '录音失败,请重试',
        icon: 'none'
      })
      this.resetRecordingState()
    })

    // 当音频真正开始播放时
    innerAudioContext.onPlay(() => {
      console.log('onPlay 音频开始播放')
      // 设置为播放状态
      this.setData({
        playStatus: 1
      })
    })

    // 绑定音频播放结束事件
    innerAudioContext.onEnded(() => {
      console.log('onEnded 音频播放结束')
      // 停止播放并重置
      this.setData({
        playStatus: 0,
        currentTime: 0,
        remain: 0
      })
      // 如果想让界面上回到音频的总时长也可以手动 set remain = recordedDuration
      // 但通常播放结束,就显示 0 或不显示都行
    })

    innerAudioContext.onTimeUpdate(() => {
      const current = Math.floor(innerAudioContext.currentTime) // 取整或保留小数都可
      const total = Math.floor(innerAudioContext.duration)

      // 若 total 不准确(部分手机可能最初获取到是 0),可做一些保护
      if (total > 0) {
        const remain = total - current
        this.setData({
          currentTime: current,
          remain: remain > 0 ? remain : 0
        })
      }
    })

    // 绑定音频播放错误事件
    innerAudioContext.onError(err => {
      console.error('播放错误:', err)
      wx.showToast({
        title: '播放失败,请重试',
        icon: 'none'
      })
      this.setData({
        playStatus: 0,
        currentTime: 0,
        remain: 0
      })
    })
  },
  /**
   * 生命周期函数--监听页面卸载
   */
  onUnload() {
    this.clearAllTimers()
    recorderManager.stop()
    innerAudioContext.stop()
    innerAudioContext.destroy()
  },
})
// index.wxml
<view wx:else class="voice-input">
          <view wx:if="{{tempFilePath !== ''}}" class="voice-msg" bind:tap="playBtn">
            <image
              src="{{ playStatus === 0 ? '/sendingaudio.png' : '/voice.gif' }}"
              mode="aspectFill"
              style="transform: rotate(180deg); width: 22rpx; height: 32rpx"
            />
            <text class="voice-msg-text"
              >{{ playStatus === 1 ? (remain + "''") : (recordedDuration + "''") }}
            </text>
          </view>
          <view
            class="voice-input-btn {{longPress == '1' ? '' : 'record-btn-2'}}"
            bind:longpress="longpressBtn"
            bind:touchend="touchendBtn"
          >
            <!-- 语音音阶动画 -->
            <view class="prompt-layer prompt-layer-1" wx:if="{{longPress == '2'}}">
              <!-- <view class="prompt-layer prompt-layer-1" wx:if="{{longPress == '2'}}"> -->

              <view class="prompt-loader">
                <view
                  class="em"
                  wx:for="{{animationArray}}"
                  wx:key="index"
                  style="--delay: {{item.delay}}s;"
                ></view>
              </view>
              <text class="p"
                >{{'剩余:' + count + 's' + (warningShown ? ',即将结束录音' : '')}}</text
              >
              <text class="span">松手结束录音</text>
            </view>
            <text class="voice-input-btn-text"
              >{{longPress == '1' ? '按住 说话' : '说话中...'}}</text
            >
          </view>
        </view>
/*  index.wxss */
.voice-btn {
  box-sizing: border-box;
  padding: 6rpx 16rpx;
  background: #2197ee;
  border-radius: 28rpx;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10rpx;
}

.voice-text {
  line-height: 42rpx;
  color: #ffffff;
  font-size: 30rpx;
}

.voice-input {
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  padding: 30rpx 76rpx;
}

.voice-msg {
  width: 100%;
  height: 56rpx;
  background: #95ec69;
  border-radius: 10rpx;
  box-shadow: 0 3rpx 6rpx rgba(0, 0, 0, 0.13);
  margin-bottom: 26rpx;
  box-sizing: border-box;
  padding: 0 20rpx;
  display: flex;
  align-items: center;
  gap: 16rpx;
}

.voice-msg-text {
  color: #000000;
  font-size: 30rpx;
  line-height: 56rpx;
}

.voice-input-btn {
  width: 100%;
  box-sizing: border-box;
  padding: 12rpx 0;
  background: #ffffff;
  border: 2rpx solid;
  border-color: #1f75e3;
  border-radius: 8rpx;
  box-sizing: border-box;
  text-align: center;
  position: relative;
}

.voice-input-btn-text {
  color: #1f75e3;
  font-size: 36rpx;
  line-height: 50rpx;
}

/* 提示小弹窗 */
.prompt-layer {
  border-radius: 16rpx;
  background: #2197ee;
  padding: 16rpx 32rpx;
  box-sizing: border-box;
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
}

.prompt-layer::after {
  content: '';
  display: block;
  border: 12rpx solid rgba(0, 0, 0, 0);
  border-top-color: #2197ee;
  position: absolute;
  bottom: -20rpx;
  left: 50%;
  transform: translateX(-50%);
}

.prompt-layer-1 {
  font-size: 32rpx;
  width: 80%;
  text-align: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  top: -178rpx;
}
.prompt-layer-1 .p {
  color: #ffffff;
}
.prompt-layer-1 .span {
  color: rgba(255, 255, 255, 0.6);
}

/* 语音音阶------------- */
/* 容器样式 */
.prompt-loader {
  width: 250rpx;
  height: 40rpx;
  display: flex;
  align-items: center; /* 对齐到容器底部 */
  justify-content: space-between;
  margin-bottom: 12rpx;
}

/* 音阶条样式 */
.prompt-loader .em {
  background: #ffffff;
  width: 6rpx;
  border-radius: 6rpx;
  height: 40rpx;
  margin-right: 5rpx;

  /* 通用动画属性 */
  animation: load 2.5s infinite linear;
  animation-delay: var(--delay);
  will-change: transform;
  transform-origin: center
}

/* 移除最后一个元素的右边距 */
.prompt-loader .em:last-child {
  margin-right: 0;
}

/* 动画关键帧 */
@keyframes load {
  0% {
    transform: scaleY(1);
  }
  50% {
    transform: scaleY(0.1);
  }
  100% {
    transform: scaleY(1);
  }
}
.record-btn-2 {
  background-color: rgba(33, 151, 238, 0.2);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/950949.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

MySQL学习笔记(二)

一、SQL-函数 函数-介绍 函数是指一段可以直接被另一段程序调用的程序或代码。 字符串函数 示例 --concat select concat(Hello,MySql); --upper select upper(Hello); --lpad select lpad(01,5,-); --trim select trim( Hello MySQL ); --中间空格还在&#xff0c;头尾…

java mail 535 Login Fail. Please enter your authorization code to login

报错信息提示查看 https://service.mail.qq.com/detail/0/53 帮助页面意思就是说你要使用授权码登录, 但是授权码我已经正确的设置上去了 后面从 QQ邮箱出现错误 Please enter your authorization code to_邮件群发-双翼邮件群发软件官方网 看到 账户 需要是 QQ号 例如…

mysql、postgresql、druid链接池踩坑记录

The last packet successfully received from the server wIs 10,010 milliseconds ago. The last packet sent successfully to the server was 10,010 milliseconds ago.### The error may exist in URL mysql 链接字符串没有 &connectTimeout600000&socketTimeout6…

安卓NDK视觉开发——手机拍照文档边缘检测实现方法与库封装

一、项目创建 创建NDK项目有两种方式&#xff0c;一种从新创建整个项目&#xff0c;一个在创建好的项目添加NDK接口。 1.创建NDK项目 创建 一个Native C项目&#xff1a; 选择包名、API版本与算法交互的语言&#xff1a; 选择C版本&#xff1a; 创建完之后&#xff0c;可…

Spring Boot教程之五十二:CrudRepository 和 JpaRepository 之间的区别

Spring Boot – CrudRepository 和 JpaRepository 之间的区别 Spring Boot建立在 Spring 之上&#xff0c;包含 Spring 的所有功能。由于其快速的生产就绪环境&#xff0c;使开发人员能够直接专注于逻辑&#xff0c;而不必费力配置和设置&#xff0c;因此如今它正成为开发人员…

【网页自动化】篡改猴入门教程

安装篡改猴 打开浏览器扩展商店&#xff08;Edge、Chrome、Firefox 等&#xff09;。搜索 Tampermonkey 并安装。 如图安装后&#xff0c;浏览器右上角会显示一个带有猴子图标的按钮。 创建用户脚本 已进入篡改猴管理面板点击创建 脚本注释说明 name&#xff1a;脚本名称。…

spark汇总

目录 描述运行模式1. Windows模式代码示例 2. Local模式3. Standalone模式 RDD描述特性RDD创建代码示例&#xff08;并行化创建&#xff09;代码示例&#xff08;读取外部数据&#xff09;代码示例&#xff08;读取目录下的所有文件&#xff09; 算子DAGSparkSQLSparkStreaming…

Spring AMQP-保证发送者消息的可靠性

1. 消息发送者的可靠性 保证消息的可靠性可以通过发送者重连和发送者确认来实现 发送者重连 发送者重连机制就是在发送信息的时候如果连接不上mq不会立即结束&#xff0c;而是会在一定的时间间隔之类进行重新连接&#xff0c;连接的次数和时间都是由我们在配置文件中指定的&…

vs2022编译webrtc步骤

1、主要步骤说明 概述&#xff1a;基础环境必须有&#xff0c;比如git&#xff0c;Powershell这些&#xff0c;就不写到下面了。 1.1 安装vs2022 1、选择使用C的桌面开发 2、 Windows 10 SDK安装10.0.20348.0 3、勾选MFC及ATL这两项 4、 安装完VS2022后&#xff0c;必须安…

UnityWebGl:打包成webgl后UGUI不显示文字(中文)问题

是由于unity默认使用的是Arial,导致打包成webgl时中文不显示 解决方案&#xff1a; 可在电脑C盘下&#xff0c;路径为C:\Windows\Fonts 找个中文简体的字体文件放到unity里面&#xff0c;格式必须为. ttf

ffmpeg-avio实战:打开本地文件或者网络直播流dome

使用ffmpeg打开打开本地文件或者网络直播流的一个小dome。流程产靠ffmpeg4.x系列的解码流程-CSDN博客 #include <libavcodec/avcodec.h> #include <libavformat/avformat.h> #include <libavformat/avio.h> #include <libavutil/file.h> #include &l…

英伟达打造个人 AI 超级计算机:Project DIGITS 震撼登场

手掌大小的超级计算机 Nvidia 在 CES 2025 上为桌面用户推出了 一款大小和手掌差不多的超级计算机——Project DIGITS AI 超级计算机。虽然它的大小和一个手掌差不多&#xff0c;但性能方面可以说是强到惊人。 Project DIGITS Project DIGITS Project DIGITS 搭载全新的 GB10 G…

SAP SD学习笔记26 - 贩卖契约(框架协议)的概要,基本契约 - 数量契约

上一章讲了品目阶层&#xff08;产品层次结构&#xff09;&#xff0c;品揃Module(分类模块) 。 SAP SD学习笔记25 - 品目阶层&#xff08;产品层次结构&#xff09;、品揃Module&#xff08;分类模块&#xff09;-CSDN博客 本章继续讲SAP的知识&#xff1a;贩卖契约&#xff…

ESP32 IDF VScode出现头文件“无法打开 源 文件 ”,并有红色下划线警告

问题背景&#xff1a; ESP32 IDF VScode出现头文件“无法打开 源 文件 ”&#xff0c;并有红色下划线警告&#xff1a; 解决办法&#xff1a; 在工程里面的.vscode文件夹下&#xff0c;检查是否存在c_cpp_properties.json文件&#xff0c;如果没有可以手动创建添加。如图…

GaussDB事务和并发控制机制

目录 一、并发控制机制 二、MVCC实现方式 三、快照实现方式 四、GaussDB的并发控制机制 五、GaussDB基于事务提交时间戳的MVCC和快照机制 六、GaussDB分布式事务 七、总结与展望 事务是数据库的核心功能之一&#xff0c;其主要目的是保障数据库系统在并发处理、系统故障…

【YOLOv8老鼠检测】

YOLOv8老鼠检测 yolo老鼠检测数据集和模型YOLOv8老鼠检测步骤YOLOv8算法说明 yolo老鼠检测数据集和模型 数据集类别信息 train: E:\python_code\dataset_1\yolo_mouse_data_5000\train/images val: E:\python_code\dataset_1\yolo_mouse_data_5000\valid/images test: E:\pyt…

2025最新解决方案:新买的mac鼠标和这个触控板反向

solution1 &#xff1a;1.打开设置&#xff0c;搜索 触控 点击 自然滚动 ----->解决的是 触控板 但是还是解决不了鼠标反向的问题 solution1 ultra&#xff1a; 下载一个免费 且纯净的 软件 Scroll Reverser for macOS 这是给出的链接&#xff0c;非常简单&#xff0c;…

【C++习题】20. 两个数组的交集

题目&#xff1a;349. 两个数组的交集 - 力扣&#xff08;LeetCode&#xff09; 链接&#x1f517;&#xff1a;349. 两个数组的交集 - 力扣&#xff08;LeetCode&#xff09; 题目&#xff1a; 代码&#xff1a; class Solution { public:// 函数功能&#xff1a;求两个数组…

从零开始:使用VSCode搭建Python数据科学开发环境

引言 在数据科学领域&#xff0c;一个高效、稳定的开发环境是成功的关键。本文将详细介绍如何使用Visual Studio Code搭建一个完整的Python数据科学开发环境。通过本指南&#xff0c;您将学会&#xff1a; 安装和配置VSCode&#xff0c;包括基本设置和快捷键配置设置Python开…

JVM vs JDK vs JRE

JVM是Java虚拟机的缩写&#xff0c; 用于实现Java的一次编译&#xff0c;处处运行。 Java代码写成.class后&#xff0c;由本地的虚拟机运行。 JDK&#xff08;Java Development Kit&#xff09;是一个功能齐全的 Java 开发工具包&#xff0c;供开发者使用。 JDK包含了JRE。…