微信小程序实现长按录音,点击播放等功能,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);
}

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Ubuntu 20.04安装gcc

一、安装GCC 1.更新包列表 user596785154:~$ sudo apt update2.安装gcc user596785154:~$ sudo apt install gcc3.验证安装 user596785154:~$ gcc --version二 编译C文件 1.新建workspace文件夹 user596785154:~$ mkdir workspace2.进入workspace文件夹 user596785154:~…

计算机网络 (23)IP层转发分组的过程

一、IP层的基本功能 IP层&#xff08;Internet Protocol Layer&#xff09;是网络通信模型中的关键层&#xff0c;属于OSI模型的第三层&#xff0c;即网络层。它负责在不同网络之间传输数据包&#xff0c;实现网络间的互联。IP层的主要功能包括寻址、路由、分段和重组、错误检测…

国产游戏崛起,燕云十六移动端1.9上线,ToDesk云电脑先开玩

游戏爱好者的利好消息出新了&#xff01;网易大型武侠仙游《燕云十六声》正式官宣&#xff0c;移动端要在1月9日正式上线了&#xff01;你期待手游版的燕云吗&#xff1f;不妨评论区留言说说你的看法。小编分别花了几个小时在台式机电脑和手机上都试了下&#xff0c;欣赏画面还…

【HarmonyOS NEXT】鸿蒙应用实现屏幕录制详解和源码

【HarmonyOS NEXT】鸿蒙应用实现屏幕录制详解和源码 一、前言 官方文档关于屏幕录制的API和示例介绍获取简单和突兀。使用起来会让上手程度变高。所以特意开篇文章&#xff0c;讲解屏幕录制的使用。官方文档参见&#xff1a;使用AVScreenCaptureRecorder录屏写文件(ArkTS) 二…

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号 例如…

怎样修改el-table主题样式

起因&#xff1a;el-table有主题样式&#xff0c;部分需要单独设置 环境&#xff1a;ideanodejs插件谷歌浏览器 第一步&#xff1a;找到scss文件&#xff1a; 谷歌浏览器打开表格页面&#xff0c;ctrlshifti打开开发者工具&#xff0c;点击后鼠标移动到表格单元格上单击一下…

记录一次面试中被问到的问题 (HR面)

文章目录 一、你对公司的了解多少二、为什么对这个岗位感兴趣三、不能说的离职原因四、离职原因高情商回复五、你的核心优势是什么六、你认为你比其他面试候选人的优势是什么七、不要提及情感 一、你对公司的了解多少 准备要点&#xff1a; 在面试前&#xff0c;对公司进行充分…

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

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

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

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

【深度学习】深度(Deep Learning)学习基础

深度学习&#xff08;Deep Learning&#xff09; 深度学习是一种基于人工神经网络的机器学习方法&#xff0c;通过多个层次&#xff08;深度&#xff09;的神经网络从数据中自动学习特征和模式。它是人工智能的一个核心领域&#xff0c;尤其在处理复杂数据&#xff08;如图像、…

【MySQL 保姆级教学】用户管理和数据库权限(16)

数据库账户管理是指对数据库用户进行创建、修改和删除等操作&#xff0c;以控制用户对数据库的访问权限。通过账户管理&#xff0c;可以设置用户名、密码、主机地址等信息&#xff0c;确保数据库的安全性和可控性。例如&#xff0c;使用 CREATE USER 创建用户&#xff0c;ALTER…

STM32+WIFI获取网络时间+8位数码管显示+0.96OLED显

资料下载地址&#xff1a;STM32WIFI获取网络时间8位数码管显示0.96OLED 1、项目介绍 主控芯片STM32C8T6 接线&#xff1a;串口1&#xff1a;PA9 PA10 OELD &#xff1a;PB6 PB7 数码管使用&#xff1a;MAX7219 8位数码管 Max7219_pinCLK PAout(5) Max7219_pinC…

决定系数(R²分数)——评估回归模型性能的一个指标

目录 1.定义 2.计算举例 3. 结果分析 1.定义 R&#xff08;R平方&#xff09;分数&#xff0c;也称为决定系数&#xff0c;是用来评估回归模型性能的一个指标。它表示自变量解释因变量变异性的比例。R分数的取值范围通常在0到1之间&#xff0c;其值越接近1&#xff0c;说明…

代码随想录算法训练营day23

代码随想录算法训练营 —day23 文章目录 代码随想录算法训练营前言一、39. 组合总和二、40.组合总和II三、131.分割回文串总结 前言 今天是算法营的第23天&#xff0c;希望自己能够坚持下来&#xff01; 今日任务&#xff1a; ● 39. 组合总和 ● 40.组合总和II ● 131.分割回…

【电子通识】PWM驱动让有刷直流电机恒流工作

电机的典型驱动方法包括电压驱动、电流驱动以及PWM驱动。本文将介绍采用PWM驱动方式的恒流工作。 首先介绍的是什么是PWM驱动的电机恒流工作&#xff0c;其次是PWM驱动电机恒流工作时电路的工作原理。 PWM驱动 当以恒定的电流驱动电机时&#xff0c;电机会怎样工作呢&#xff1…

基于html5实现音乐录音播放动画源码

源码介绍 基于html5实现音乐录音播放动画源码是一款类似Shazam的UI&#xff0c;点击按钮后&#xff0c;会变成为一个监听按钮。旁边会有音符飞入这个监听按钮&#xff0c;最后转换成一个音乐播放器。 效果预览 源码获取 基于html5实现音乐录音播放动画源码

精度论文:【Coordinate Attention for Efficient Mobile Network Design】

Coordinate Attention for Efficient Mobile Network Design 《用于高效移动网络设计的坐标注意力机制》1.引言2.相关工作2.1 移动网络架构2.2 注意力机制 3. 坐标注意力3.1. 回顾SE注意力 (Squeeze-and-Excitation Attention)3.2. 坐标注意力块3.2.1 坐标信息嵌入3.2.2 坐标注…

高等数学学习笔记 ☞ 一元函数微分的基础知识

1. 微分的定义 &#xff08;1&#xff09;定义&#xff1a;设函数在点的某领域内有定义&#xff0c;取附近的点&#xff0c;对应的函数值分别为和&#xff0c; 令&#xff0c;若可以表示成&#xff0c;则称函数在点是可微的。 【 若函数在点是可微的&#xff0c;则可以表达为】…

openai swarm agent框架源码详解及应用案例实战

文章目录 简介数据类型Agent类Response类Result类Swarm类run_demo_loop交互式会话 基础应用agent-handsofffunction-callingcontext_variablestriage_agent 高阶应用通用客服机器人(support bot)构建航班服务agent 参考资料 openai 在24年10月份开源了一个教育性质的多agents协…

【测试】——Cucumber入门

&#x1f4d6; 前言&#xff1a;Cucumber框架是行为驱动&#xff08;BDD&#xff09;框架的一种&#xff0c;通过自然语言站在功能使用者视角&#xff0c;描述编写测试用例。简单来说就是通过feature文件编写脚本&#xff0c;脚本对应java写的方法&#xff0c;会有一个启动器配…