123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705 |
- <template>
- <view class="modal" v-if="visible">
- <view class="modal-content">
- <view class="modal-title">麦克风检测</view>
- <view class="modal-text">请朗读下面的文字</view>
-
- <!-- 语音波形动画 -->
- <view class="wave-container" v-if="isRecording">
- <view
- class="wave-bar"
- v-for="(item, index) in waveData"
- :key="index"
- :style="{ height: item + 'rpx' }"
- ></view>
- <!-- <view class="timer">{{ formatTime(recordingTime) }}</view> -->
- </view>
-
- <!-- 待朗读文字 -->
- <view class="read-text">成为奇才,就是现在,我们开始吧。</view>
-
- <!-- 按钮 -->
- <button class="confirm-btn" @tap="handleConfirm">
- {{ isRecording ? '停止录制' : '开始录制' }}
- </button>
-
- <view class="status-message">
- {{ statusMessage }}
- </view>
- </view>
- </view>
- </template>
- <script>
- const recorderManager = uni.getRecorderManager();
- export default {
- name: 'VoiceCheckModal',
- props: {
- visible: {
- type: Boolean,
- default: false
- }
- },
- data() {
- return {
- isRecording: false,
- waveData: Array(30).fill(20),
- recordingTime: 0,
- statusMessage: '点击"开始录制"按钮朗读文字',
- silenceCounter: 0,
- recordingTimer: null,
- volumeLevel: 0,
- animationTimer: null,
- lastVolume: 0,
- hasPermission: false, // 添加录音权限状态
- noSoundTimeout: null, // 添加无声音超时计时器
- recordingStarted: false, // 添加录音开始标志
- audioPath: null, // 添加录音文件路径
- platform: '', // 添加平台标识
- isRetrying: false // 添加重试标记
- }
- },
- methods: {
- // 检查录音权限 - 跨平台兼容版本
- async checkPermission() {
- // 获取系统信息
- const systemInfo = uni.getSystemInfoSync();
- const platform = systemInfo.platform;
-
- try {
- // iOS平台处理
- if (platform === 'ios') {
- // iOS使用getSystemSetting检查权限状态
- const res = await new Promise((resolve, reject) => {
- uni.getSetting({
- success: (result) => {
- if (result.authSetting['scope.record']) {
- resolve(true);
- } else {
- // 请求权限
- uni.authorize({
- scope: 'scope.record',
- success: () => resolve(true),
- fail: (err) => reject(err)
- });
- }
- },
- fail: (err) => reject(err)
- });
- });
- this.hasPermission = true;
- return true;
- }
- // 安卓和鸿蒙平台处理
- else {
- const res = await uni.authorize({
- scope: 'scope.record'
- });
- this.hasPermission = true;
- return true;
- }
- } catch (error) {
- console.error('权限请求失败:', error);
- uni.showModal({
- title: '提示',
- content: '需要录音权限才能进行录音,请在设置中开启录音权限',
- confirmText: '去设置',
- success: (res) => {
- if (res.confirm) {
- uni.openSetting();
- }
- }
- });
- this.hasPermission = false;
- return false;
- }
- },
- async startRecording() {
- // 检查权限
- if (!await this.checkPermission()) {
- return;
- }
- this.isRecording = true;
- this.recordingTime = 0;
- this.silenceCounter = 0;
- this.statusMessage = '正在检测声音...';
- this.waveData = this.waveData.map(() => 20);
- this.lastVolume = 0;
- this.recordingStarted = true;
-
- // 获取系统信息以适配不同平台
- const systemInfo = uni.getSystemInfoSync();
- const platform = systemInfo.platform;
-
- // 根据不同平台配置录音参数
- const recorderOptions = {
- duration: 60000, // 最长录音时间
- format: 'mp3', // 使用mp3格式提高兼容性
- frameSize: 5
- };
-
- // iOS平台特殊处理
- if (platform === 'ios') {
- // iOS推荐参数
- recorderOptions.sampleRate = 44100;
- recorderOptions.numberOfChannels = 1;
- recorderOptions.encodeBitRate = 96000;
- } else {
- // 安卓和鸿蒙平台参数
- recorderOptions.sampleRate = 16000;
- recorderOptions.numberOfChannels = 1;
- recorderOptions.encodeBitRate = 48000;
- }
-
- // 开始录音
- recorderManager.start(recorderOptions);
-
- // 计时器
- this.recordingTimer = setInterval(() => {
- this.recordingTime++;
- }, 1000);
- // 动画定时器
- this.animationTimer = setInterval(() => {
- if (this.isRecording) {
- this.updateWaveform();
- }
- }, 50);
- // 设置无声音检测超时
- this.noSoundTimeout = setTimeout(() => {
- if (this.silenceCounter > 20) {
- this.showNoSoundTip();
- }
- }, 3000);
- },
-
- stopRecording() {
- this.isRecording = false;
- this.recordingStarted = false;
- recorderManager.stop();
- clearInterval(this.recordingTimer);
- clearInterval(this.animationTimer);
- clearTimeout(this.noSoundTimeout);
- this.statusMessage = '录制完成!';
-
- setTimeout(() => {
- this.waveData = Array(30).fill(20);
- }, 500);
- // 延迟触发完成事件,确保audioPath已经设置
- setTimeout(() => {
- // 处理音频文件
- this.processAudioFile();
-
- // 触发完成事件
- this.$emit('complete', {
- success: true,
- audioPath: this.audioPath,
- platform: this.platform
- });
- }, 300);
- },
-
- // 添加音频文件处理方法
- processAudioFile() {
- if (!this.audioPath) return;
-
- // 获取系统信息
- const systemInfo = uni.getSystemInfoSync();
- this.platform = systemInfo.platform;
-
- console.log('处理音频文件:', this.audioPath, '平台:', this.platform);
-
- // 针对不同平台的音频文件处理
- if (this.platform === 'ios') {
- // iOS平台特殊处理
- if (!this.audioPath.startsWith('file://')) {
- this.audioPath = 'file://' + this.audioPath;
- }
- } else if (this.platform === 'android') {
- // 安卓平台特殊处理
- // 通常不需要特殊处理,但如果有特殊需求可以在这里添加
- } else if (this.platform === 'harmony') {
- // 鸿蒙平台特殊处理
- // 根据鸿蒙系统的特性进行处理
- }
-
- // 验证音频文件是否存在
- uni.getFileInfo({
- filePath: this.audioPath,
- success: (res) => {
- console.log('音频文件信息:', res);
- if (res.size === 0) {
- console.warn('警告: 音频文件大小为0');
- // 可以选择重新录制或提示用户
- }
- },
- fail: (err) => {
- console.error('获取音频文件信息失败:', err);
- // 处理文件不存在的情况
- }
- });
- },
- // 显示无声音提示
- showNoSoundTip() {
- if (!this.isRecording) return;
-
- uni.showModal({
- title: '未检测到声音',
- content: '请检查以下问题:\n1. 麦克风是否正常工作\n2. 是否允许使用麦克风\n3. 是否靠近麦克风说话\n4. 说话音量是否足够',
- showCancel: true,
- cancelText: '重新录制',
- confirmText: '继续录制',
- success: (res) => {
- if (res.cancel) {
- // 重新录制:停止当前录音并重新开始
- this.restartRecording();
- } else {
- // 继续录制:重置计数器继续当前录音
- this.continueRecording();
- }
- }
- });
- },
- // 重新录制方法
- restartRecording() {
- // 先停止当前录音
- recorderManager.stop();
- clearInterval(this.recordingTimer);
- clearInterval(this.animationTimer);
- clearTimeout(this.noSoundTimeout);
-
- // 重置所有状态
- this.isRecording = false;
- this.recordingStarted = false;
- this.recordingTime = 0;
- this.silenceCounter = 0;
- this.volumeLevel = 0;
- this.lastVolume = 0;
- this.waveData = Array(30).fill(20);
-
- // 短暂延迟后开始新的录音
- setTimeout(() => {
- this.startRecording();
- }, 500);
- },
- // 继续录制方法
- continueRecording() {
- // 重置计数器和状态,但保持录音继续
- this.silenceCounter = 0;
- this.statusMessage = '正在检测声音...';
-
- // 重新设置无声音检测超时
- clearTimeout(this.noSoundTimeout);
- this.noSoundTimeout = setTimeout(() => {
- if (this.silenceCounter > 20) {
- this.showNoSoundTip();
- }
- }, 3000);
- },
- detectAudio(int16Array) {
- // 获取系统信息
- const systemInfo = uni.getSystemInfoSync();
- const platform = systemInfo.platform;
-
- // 计算平均音量 - 优化算法以提高准确性
- let sum = 0;
- let peak = 0;
-
- // 使用分块处理以提高性能
- const blockSize = 128;
- for (let i = 0; i < int16Array.length; i += blockSize) {
- const end = Math.min(i + blockSize, int16Array.length);
- for (let j = i; j < end; j++) {
- const absValue = Math.abs(int16Array[j]);
- sum += absValue;
- peak = Math.max(peak, absValue);
- }
- }
-
- const avg = sum / int16Array.length;
-
- // 根据不同平台调整阈值
- let SPEAK_THRESHOLD = 500; // 默认阈值
- let volumeScale = 2000;
-
- if (platform === 'ios') {
- SPEAK_THRESHOLD = 300; // iOS通常需要更低的阈值
- volumeScale = 1500;
- } else if (platform === 'android') {
- SPEAK_THRESHOLD = 500; // 安卓标准阈值
- volumeScale = 2000;
- } else {
- // 鸿蒙或其他平台
- SPEAK_THRESHOLD = 400;
- volumeScale = 1800;
- }
-
- // 使用峰值和平均值的组合来计算音量级别
- const combinedLevel = (avg * 0.7 + peak * 0.3) / volumeScale;
- this.volumeLevel = this.smoothVolume(combinedLevel);
-
- // 更新状态
- if (avg > SPEAK_THRESHOLD || peak > SPEAK_THRESHOLD * 3) {
- this.statusMessage = '检测到声音:录音中...';
- this.silenceCounter = 0;
- clearTimeout(this.noSoundTimeout); // 清除无声音超时
-
- // 立即更新波形以反映实际声音
- this.updateWaveform(false);
- } else {
- this.silenceCounter++;
- if (this.silenceCounter > 10) {
- this.statusMessage = '未检测到声音,请靠近麦克风并说话...';
- // 如果持续30次都没有声音,显示提示
- if (this.silenceCounter === 30 && this.isRecording) {
- this.showNoSoundTip();
- }
-
- // 静音时逐渐降低波形
- this.updateWaveform(true);
- }
- }
- },
- // 添加音量平滑处理函数
- smoothVolume(newVolume) {
- const smoothFactor = 0.3; // 平滑系数
- const smoothedVolume = this.lastVolume + (newVolume - this.lastVolume) * smoothFactor;
- this.lastVolume = smoothedVolume;
- return Math.min(1, Math.max(0, smoothedVolume));
- },
-
- updateWaveform(isSilent = false) {
- // 获取系统信息以优化动画性能
- const systemInfo = uni.getSystemInfoSync();
- const platform = systemInfo.platform;
-
- // 根据平台调整动画参数
- let decayFactor = 0.95; // 静音时波形衰减因子
- let animationScale = 1.0; // 动画缩放因子
-
- if (platform === 'ios') {
- // iOS上动画通常需要更平滑
- decayFactor = 0.97;
- animationScale = 0.9;
- } else if (platform === 'android') {
- // 安卓标准参数
- decayFactor = 0.95;
- animationScale = 1.0;
- } else {
- // 鸿蒙或其他平台
- decayFactor = 0.96;
- animationScale = 0.95;
- }
-
- if (isSilent) {
- // 静音时波形逐渐降低
- this.waveData = this.waveData.map(height => Math.max(20, height * decayFactor));
- } else {
- const now = Date.now() * 0.002;
-
- // 使用当前音量级别动态调整波形
- const volumeEffect = this.volumeLevel * animationScale;
- const baseHeight = 20 + volumeEffect * 60;
- const waveAmplitude = volumeEffect * 40;
-
- // 更新波形数据
- this.waveData = this.waveData.map((currentHeight, i) => {
- // 使用余弦和正弦的组合来创建更流畅的波形
- const phase = i * 0.2 + now;
- const wave1 = Math.cos(phase) * 0.5;
- const wave2 = Math.sin(phase * 1.5) * 0.3;
-
- // 添加随机因素使波形更自然
- const randomFactor = Math.random() * 0.1;
- const combinedWave = (wave1 + wave2 + randomFactor) * volumeEffect;
-
- // 计算目标高度
- const targetHeight = baseHeight + combinedWave * waveAmplitude;
-
- // 平滑过渡到新高度 (LERP - 线性插值)
- const smoothFactor = platform === 'ios' ? 0.3 : 0.4; // iOS上更平滑
- const newHeight = currentHeight + (targetHeight - currentHeight) * smoothFactor;
-
- // 确保高度在合理范围内
- return Math.max(20, Math.min(120, newHeight));
- });
- }
- },
- handleConfirm() {
- if (!this.isRecording) {
- this.startRecording();
- } else {
- this.stopRecording();
- }
- },
-
- formatTime(seconds) {
- const mins = Math.floor(seconds / 60);
- const secs = seconds % 60;
- return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
- }
- },
- mounted() {
- // 获取系统信息
- const systemInfo = uni.getSystemInfoSync();
- const platform = systemInfo.platform;
-
- // 监听录音错误 - 增强错误处理
- recorderManager.onError((error) => {
- console.error('录音错误:', error);
-
- // 根据不同平台处理错误
- let errorMessage = '录音出现错误,请检查麦克风权限或重试';
-
- // 针对iOS的特定错误处理
- if (platform === 'ios') {
- if (error.errMsg && error.errMsg.includes('authorize')) {
- errorMessage = 'iOS需要麦克风权限,请在设置中允许访问麦克风';
- } else if (error.errMsg && error.errMsg.includes('busy')) {
- errorMessage = '麦克风正被其他应用使用,请关闭其他使用麦克风的应用';
- }
- }
- // 针对安卓的特定错误处理
- else if (platform === 'android') {
- if (error.errMsg && error.errMsg.includes('permission')) {
- errorMessage = '安卓系统需要麦克风权限,请在设置中允许访问麦克风';
- }
- }
- // 针对鸿蒙的特定错误处理
- else if (platform === 'harmony') {
- errorMessage = '请确保已授予麦克风权限并重试';
- }
-
- uni.showModal({
- title: '录音错误',
- content: errorMessage,
- showCancel: false,
- success: () => {
- this.stopRecording();
- }
- });
- });
- // 监听录音开始
- recorderManager.onStart(() => {
- console.log('录音开始');
- this.recordingStarted = true;
-
- // 平台特定处理
- if (platform === 'ios') {
- // iOS上可能需要额外延迟来确保录音真正开始
- setTimeout(() => {
- // 如果没有检测到声音,开始模拟波形动画
- if (this.silenceCounter > 5) {
- this.updateWaveform(false);
- }
- }, 500);
- }
- });
- // 监听录音帧数据
- recorderManager.onFrameRecorded(res => {
- if (!res.frameBuffer || !this.recordingStarted) return;
-
- try {
- const int16Array = new Int16Array(res.frameBuffer);
- this.detectAudio(int16Array);
- } catch (error) {
- console.error('处理音频帧数据错误:', error);
- // 出错时使用默认波形动画
- this.updateWaveform(false);
- }
- });
-
- // 录音结束回调
- recorderManager.onStop(res => {
- this.audioPath = res.tempFilePath;
- this.isRecording = false;
- this.recordingStarted = false;
- clearInterval(this.recordingTimer);
- clearInterval(this.animationTimer);
- clearTimeout(this.noSoundTimeout);
- this.statusMessage = '录制完成!';
-
- // 确保在所有平台上都能正确获取录音文件
- if (res.tempFilePath) {
- console.log('录音文件路径:', res.tempFilePath);
-
- // 针对iOS的特殊处理
- if (platform === 'ios' && !res.tempFilePath.startsWith('file://')) {
- this.audioPath = 'file://' + res.tempFilePath;
- } else {
- this.audioPath = res.tempFilePath;
- }
- } else {
- console.error('未获取到录音文件路径');
- uni.showToast({
- title: '录音保存失败',
- icon: 'none'
- });
- }
- });
-
- // 添加中断处理 - 主要针对iOS
- recorderManager.onInterruptionBegin && recorderManager.onInterruptionBegin(() => {
- console.log('录音被中断');
- this.statusMessage = '录音被中断,请重试';
- this.stopRecording();
- });
-
- recorderManager.onInterruptionEnd && recorderManager.onInterruptionEnd(() => {
- console.log('录音中断结束');
- // 可以选择自动重新开始录音
- // this.startRecording();
- });
- },
- beforeDestroy() {
- // 停止所有计时器
- clearInterval(this.recordingTimer);
- clearInterval(this.animationTimer);
- clearTimeout(this.noSoundTimeout);
-
- // 确保录音停止
- if (this.isRecording) {
- recorderManager.stop();
- }
-
- // 移除事件监听器
- try {
- // 注意:uni-app不支持直接移除事件监听器
- // 但我们可以确保组件销毁时停止所有活动
- this.isRecording = false;
- this.recordingStarted = false;
- } catch (error) {
- console.error('清理资源时出错:', error);
- }
- }
- }
- </script>
- <style scoped>
- .modal {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: rgba(0, 0, 0, 0.6);
- display: flex;
- align-items: center;
- justify-content: center;
- z-index: 999;
- }
- .modal-content {
- width: 80%;
- background: #fff;
- border-radius: 20rpx;
- padding: 40rpx;
- text-align: center;
- box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.2);
- }
- .modal-title {
- font-size: 36rpx;
- font-weight: bold;
- margin-bottom: 20rpx;
- color: #0039b3;
- }
- .modal-text {
- font-size: 30rpx;
- color: #666;
- margin-bottom: 30rpx;
- }
- .wave-container {
- height: 200rpx;
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 4rpx; /* 减小波形条之间的间距 */
- margin: 30rpx 0;
- background: #f8f9fa;
- border-radius: 15rpx;
- position: relative;
- overflow: hidden;
- border: 1rpx solid #eee;
- }
- .wave-bar {
- width: 6rpx; /* 减小波形条的宽度 */
- background: linear-gradient(to top, #1a73e8, #0039b3);
- border-radius: 3rpx;
- transition: height 0.1s ease-out; /* 加快过渡动画速度 */
- }
- .timer {
- position: absolute;
- top: 15rpx;
- right: 15rpx;
- background: rgba(0, 57, 179, 0.1);
- color: #0039b3;
- padding: 5rpx 15rpx;
- border-radius: 20rpx;
- font-size: 26rpx;
- font-weight: 500;
- }
- .read-text {
- background: #f8f9fa;
- padding: 30rpx;
- border-radius: 15rpx;
- font-size: 32rpx;
- margin: 30rpx 0;
- color: #333;
- line-height: 1.6;
- border: 1rpx solid #eee;
- }
- .confirm-btn {
- width: 80%;
- height: 80rpx;
- line-height: 80rpx;
- background: linear-gradient(90deg, #0039b3, #1a73e8);
- color: #fff;
- border-radius: 40rpx;
- margin-top: 30rpx;
- font-size: 32rpx;
- font-weight: 500;
- box-shadow: 0 5rpx 15rpx rgba(0, 57, 179, 0.3);
- }
- .status-message {
- font-size: 28rpx;
- color: #666;
- margin-top: 30rpx;
- height: 40rpx;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- /* 添加状态消息的动画效果 */
- @keyframes blink {
- 0% { opacity: 1; }
- 50% { opacity: 0.5; }
- 100% { opacity: 1; }
- }
- .status-message.warning {
- color: #ff9800;
- animation: blink 1.5s infinite;
- }
- </style>
|