微信小程序实现长按录音,点击播放等功能,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 - currentTimewarningShown: false, // 是否已显示50秒提示minDuration: 2, // 录音最小时长秒数animationArray: Array.from({ length: 15 }, (_, index) => {// length 这个名字就不再需要,因为我们已经在这里写死了 15const centerIndex = Math.floor((15 - 1) / 2) // 7const distance = Math.abs(index - centerIndex)// 中心延迟为 0,向外越来越大const delay = distance * 0.2return { 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.timeif (duration >= this.data.minDuration) {this.setData({recordedDuration: duration,tempFilePath: newTempFilePath},() => {console.log('录音已停止,时长:', this.data.recordedDuration, '秒')})} else {// 录音时长过短,提示用户wx.showToast({title: '录音时间太短',icon: 'none'})// 不覆盖之前的 tempFilePath,保留旧的录音// 仅重置 timethis.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.datasetthis.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, // 在开始录音前重置 timewarningShown: false // 重置警告提示})this.startCountdown(this.data.duration / 1000) // 录音倒计时60秒//recorderManager.stop() // 确保之前的录音已停止this.startRecordingTimer()const options = {duration: this.data.duration * 1000, // 指定录音的时长,单位 mssampleRate: 16000, // 采样率numberOfChannels: 1, // 录音通道数encodeBitRate: 96000, // 编码码率format: 'mp3', // 音频格式,有效值 aac/mp3frameSize: 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 => {// 将新录音的文件路径传递给 stopRecordingTimerthis.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 - currentthis.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"><imagesrc="{{ 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><viewclass="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"><viewclass="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);
}
相关文章:
微信小程序实现长按录音,点击播放等功能,CSS实现语音录制动画效果
有一个需求需要在微信小程序上实现一个长按时进行语音录制,录制时间最大为60秒,录制完成后,可点击播放,播放时再次点击停止播放,可以反复录制,新录制的语音把之前的语音覆盖掉,也可以主动长按删…...

校园跑腿小程序---轮播图,导航栏开发
hello hello~ ,这里是 code袁~💖💖 ,欢迎大家点赞🥳🥳关注💥💥收藏🌹🌹🌹 🦁作者简介:一名喜欢分享和记录学习的在校大学生…...

详细全面讲解C++中重载、隐藏、覆盖的区别
文章目录 总结1、重载示例代码特点1. 模板函数和非模板函数重载2. 重载示例与调用规则示例代码调用规则解释3. 特殊情况与注意事项二义性问题 函数特化与重载的交互 2. 函数隐藏(Function Hiding)概念示例代码特点 3. 函数覆盖(重写ÿ…...

一文读懂单片机的串口
目录 串口通信的基本概念 串口通信的关键参数 单片机串口的硬件连接 单片机串口的工作原理 数据发送过程 数据接收过程 单片机串口的编程实现 以51单片机为例 硬件连接 初始化串口 发送数据 接收数据 串口中断服务函数 代码示例 单片机串口的应用实例 单片机与…...

HTML5 网站模板
HTML5 网站模板 参考 HTML5 Website Templates...
mybatis分页插件:PageHelper、mybatis-plus-jsqlparser(解决SQL_SERVER2005连接分页查询OFFSET问题)
文章目录 引言I PageHelper坐标II mybatis-plus-jsqlparser坐标Spring Boot 添加分页插件自定义 Mapper 方法中使用分页注意事项解决SQL_SERVER2005连接分页查询OFFSET问题知识扩展MyBatis-Plus 框架结构mybatis-plus-jsqlparser的 Page 类引言 PageHelper import com.github.p…...
uniapp中rpx和upx的区别
在 UniApp 中,rpx 和 upx 是两种不同的单位,它们的主要区别在于适用的场景和计算方式。 ### rpx(Responsive Pixel) - **适用场景**:rpx 是一种响应式单位,主要用于小程序和移动端的布局。 - **计算方式**…...

什么是卷积网络中的平移不变性?平移shft在数据增强中的意义
今天来介绍一下数据增强中的平移shft操作和卷积网络中的平移不变性。 1、什么是平移 Shift 平移是指在数据增强(data augmentation)过程中,通过对输入图像或目标进行位置偏移(平移),让目标在图像中呈现出…...
java.net.SocketException: Connection reset 异常原因分析和解决方法
导致此异常的原因,总结下来有三种情况: 一、服务器端偶尔出现了异常,导致连接关闭 解决方法: 采用出错重试机制 二、 服务器端和客户端使用的连接方式不一致 解决方法: 服务器端和客户端使用相同的连接方式ÿ…...
Maven 仓库的分类
Maven 是一个广泛使用的项目构建和依赖管理工具,在 Java 开发生态中占据重要地位。作为 Maven 的核心概念之一,仓库(Repository)扮演着至关重要的角色,用于存储项目的依赖、插件以及构建所需的各种资源。 了解 Maven 仓…...
隧道网络:为数据传输开辟安全通道
什么是隧道网络? 想象一下,你正在一个陌生的城市旅行,并且想要访问家里的电脑。但是,直接连接是不可能的,因为家庭网络通常受到防火墙或路由器的保护,不允许外部直接访问。这时候,隧道网络&…...

CentOS 7 下 Nginx 的详细安装与配置
1、安装方式 1.1、通过编译方式安装 下载Nginx1.16.1的安装包 https://nginx.org/download/nginx-1.16.1.tar.gz 下载后上传至/home目录下。 1.2、通过yum方式安装 这种方式安装更简单。 2、通过编译源码包安装Nginx 2.1、安装必要依赖 sudo yum -y install gcc gcc-c sudo…...

JAVA 使用apache poi实现EXCEL文件的输出;apache poi实现标题行的第一个字符为红色;EXCEL设置某几个字符为别的颜色
设置输出文件的列宽,防止文件过于丑陋 Sheet sheet workbook.createSheet(FileConstants.ERROR_FILE_SHEET_NAME); sheet.setColumnWidth(0, 40 * 256); sheet.setColumnWidth(1, 20 * 256); sheet.setColumnWidth(2, 20 * 256); sheet.setColumnWidth(3, 20 * 25…...

通过vba实现在PPT中添加计时器功能
目录 一、前言 二、具体实现步骤 1、准备 2、开启宏、打开开发工具 3、添加计时器显示控件 3.1、开启母版 3.2、插入计时器控件 4、vba代码实现 4.1、添加模块 4.2、添加代码 4.3、保存为pptm 5、效果展示 一、前言 要求/目标:在PPT中每一页上面增加一个计时器功能…...

检验统计量与p值笔记
一、背景 以雨量数据为例,当获得一个站点一年的日雨量数据后,我们需要估计该站点的雨量的概率分布情况,因此我们利用有参估计的方式如极大似然法估计得到了假定该随机变量服从某一分布的参数,从而得到该站点的概率密度函数&#x…...
【集成学习】Bagging、Boosting、Stacking算法详解
文章目录 1. 相关算法详解:2. 算法详细解释:2.1 Bagging:2.2 Boosting:2.3 Stacking:2.4 K-fold Multi-level Stacking: 集成学习(Ensemble Learning)是一种通过结合多个模型的预测结…...
Rabbit Rocket kafka 怎么实现消息有序消费和延迟消费的
在消息队列系统中,像 RabbitMQ、RocketMQ 和 Kafka 这样的系统,都支持不同的方式来实现消息的有序消费和延迟消费。下面我们分别探讨这些系统中如何实现这两种需求: 1. RabbitMQ:实现消息有序消费和延迟消费 有序消费࿱…...
【Ubuntu与Linux操作系统:五、文件与目录管理】
第5章 磁盘存储管理 5.1 Linux磁盘存储概述 磁盘存储是Linux系统存储数据的重要组件,它通过分区和文件系统组织和管理数据。Linux支持多种文件系统,如ext4、xfs和btrfs,并以块的形式管理存储设备。 1. 分区与文件系统: 分区&am…...

32_Redis分片集群原理
1.Redis集群分片 1.1 Redis集群分片介绍 Redis集群没有使用一致性hash,而是引入了哈希槽的概念。Redis集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽。 用于将密钥映射到散列插槽的基本算法如下: HASH_SLOT = CRC16(key) mod 16384 集群的每…...
微信小程序mp3音频播放组件,仅需传入url即可
// index.js // packageChat/components/audio-player/index.js Component({/*** 组件的属性列表*/properties: {/*** MP3 文件的 URL*/src: {type: String,value: ,observer(newVal, oldVal) {if (newVal ! oldVal && newVal) {// 如果 InnerAudioContext 已存在&…...

eNSP-Cloud(实现本地电脑与eNSP内设备之间通信)
说明: 想象一下,你正在用eNSP搭建一个虚拟的网络世界,里面有虚拟的路由器、交换机、电脑(PC)等等。这些设备都在你的电脑里面“运行”,它们之间可以互相通信,就像一个封闭的小王国。 但是&#…...
Java 语言特性(面试系列2)
一、SQL 基础 1. 复杂查询 (1)连接查询(JOIN) 内连接(INNER JOIN):返回两表匹配的记录。 SELECT e.name, d.dept_name FROM employees e INNER JOIN departments d ON e.dept_id d.dept_id; 左…...
DockerHub与私有镜像仓库在容器化中的应用与管理
哈喽,大家好,我是左手python! Docker Hub的应用与管理 Docker Hub的基本概念与使用方法 Docker Hub是Docker官方提供的一个公共镜像仓库,用户可以在其中找到各种操作系统、软件和应用的镜像。开发者可以通过Docker Hub轻松获取所…...
【SpringBoot】100、SpringBoot中使用自定义注解+AOP实现参数自动解密
在实际项目中,用户注册、登录、修改密码等操作,都涉及到参数传输安全问题。所以我们需要在前端对账户、密码等敏感信息加密传输,在后端接收到数据后能自动解密。 1、引入依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId...

iPhone密码忘记了办?iPhoneUnlocker,iPhone解锁工具Aiseesoft iPhone Unlocker 高级注册版分享
平时用 iPhone 的时候,难免会碰到解锁的麻烦事。比如密码忘了、人脸识别 / 指纹识别突然不灵,或者买了二手 iPhone 却被原来的 iCloud 账号锁住,这时候就需要靠谱的解锁工具来帮忙了。Aiseesoft iPhone Unlocker 就是专门解决这些问题的软件&…...
基础测试工具使用经验
背景 vtune,perf, nsight system等基础测试工具,都是用过的,但是没有记录,都逐渐忘了。所以写这篇博客总结记录一下,只要以后发现新的用法,就记得来编辑补充一下 perf 比较基础的用法: 先改这…...
linux 错误码总结
1,错误码的概念与作用 在Linux系统中,错误码是系统调用或库函数在执行失败时返回的特定数值,用于指示具体的错误类型。这些错误码通过全局变量errno来存储和传递,errno由操作系统维护,保存最近一次发生的错误信息。值得注意的是,errno的值在每次系统调用或函数调用失败时…...

Cinnamon修改面板小工具图标
Cinnamon开始菜单-CSDN博客 设置模块都是做好的,比GNOME简单得多! 在 applet.js 里增加 const Settings imports.ui.settings;this.settings new Settings.AppletSettings(this, HTYMenusonichy, instance_id); this.settings.bind(menu-icon, menu…...
【学习笔记】深入理解Java虚拟机学习笔记——第4章 虚拟机性能监控,故障处理工具
第2章 虚拟机性能监控,故障处理工具 4.1 概述 略 4.2 基础故障处理工具 4.2.1 jps:虚拟机进程状况工具 命令:jps [options] [hostid] 功能:本地虚拟机进程显示进程ID(与ps相同),可同时显示主类&#x…...
docker 部署发现spring.profiles.active 问题
报错: org.springframework.boot.context.config.InvalidConfigDataPropertyException: Property spring.profiles.active imported from location class path resource [application-test.yml] is invalid in a profile specific resource [origin: class path re…...