2025-10-11 18:25:59 +08:00

471 lines
19 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.Media;
using AndroidX.Core.App;
using AndroidX.Core.Content;
using NAudio.Wave;
using System.Threading.Tasks;
using XiaoZhiSharp.Services;
namespace XiaoZhiSharp_MauiBlazorApp.Services
{
public class AudioService : IDisposable, IAudioService
{
#region
// 音频参数
private const int SampleRate = 24000;
private const int SampleRate_WaveIn = 16000;
private const int Bitrate = 16;
private const int Channels = 1;
private const int FrameDuration = 60;
private const int FrameSize = SampleRate * FrameDuration / 1000; // 帧大小
// 计算60ms的样本数
private const int SamplesPerFrame = SampleRate_WaveIn * FrameDuration / 1000;
// 16位音频每个样本2字节
private const int BytesPerSample = Bitrate / 8;
// 60ms帧的字节数
private const int BytesPerFrame = SamplesPerFrame * BytesPerSample;
private MediaPlayer? _mediaPlayer;
private AudioRecord? _audioRecord;
private AudioTrack? _audioTrack;
private bool _isPlaying;
private bool _isRecording;
// VAD相关字段从Unity版本移植
private bool useVAD = true; // 是否使用VAD
private float vadThreshold = 0.015f; // VAD阈值可根据环境噪音调整
private int vadSilenceFrames = 20; // 静音帧数阈值(约0.33秒)
private int currentSilenceFrames = 0; // 当前连续静音帧数
private bool isSpeaking = false; // 是否检测到说话
private DateTime lastTtsEndTime = DateTime.Now; // 上次TTS结束的时间
private float ttsCooldownTime = 0.5f; // TTS结束后的冷却时间(秒)
private bool isInCooldown = false; // 是否处于冷却期
private CancellationTokenSource? _recordingCts;
#endregion
#region
public event IAudioService.PcmAudioEventHandler? OnPcmAudioEvent;
#endregion
#region
public bool IsPlaying => _isPlaying;
public bool IsRecording => _isRecording;
public int VadCounter { get; private set; } = 0; // 用于语音活动检测的计数器
#endregion
public AudioService()
{
if (!HasAudioPermission())
{
RequestAudioPermission();
}
try
{
// 初始化音频播放组件
_mediaPlayer = new MediaPlayer();
// 计算最小缓冲区大小,并确保至少能容纳一帧数据
int minBufferSize = AudioRecord.GetMinBufferSize(SampleRate_WaveIn, ChannelIn.Mono, Android.Media.Encoding.Pcm16bit);
int bufferSize = Math.Max(minBufferSize, BytesPerFrame * 2); // 至少能容纳2帧数据
// 初始化音频录制组件
_audioRecord = new AudioRecord(AudioSource.Mic, SampleRate_WaveIn, ChannelIn.Mono, Android.Media.Encoding.Pcm16bit, bufferSize);
// 检查 AudioRecord 是否初始化成功
if (_audioRecord.State != State.Initialized)
{
Console.WriteLine("AudioRecord 初始化失败");
_audioRecord = null;
}
// 初始化 AudioTrack
int audioTrackBufferSize = AudioTrack.GetMinBufferSize(SampleRate, ChannelOut.Mono, Android.Media.Encoding.Pcm16bit);
_audioTrack = new AudioTrack(Android.Media.Stream.Music, SampleRate, ChannelOut.Mono, Android.Media.Encoding.Pcm16bit, audioTrackBufferSize, AudioTrackMode.Stream);
}
catch (Exception ex)
{
Console.WriteLine($"初始化音频组件时出错: {ex.Message}");
}
}
private bool HasAudioPermission()
{
return ContextCompat.CheckSelfPermission(Android.App.Application.Context, Android.Manifest.Permission.RecordAudio) == Permission.Granted;
}
private async Task RequestAudioPermission()
{
ActivityCompat.RequestPermissions(Platform.CurrentActivity, new string[] { Android.Manifest.Permission.RecordAudio }, 1);
await Task.Delay(1000); // 等待用户响应权限请求
}
public void StartRecording()
{
try
{
// 如果处于冷却期,不启动录音
if (isInCooldown)
{
Console.WriteLine("处于冷却期,暂不启动录音");
return;
}
// 如果已经在录音,先停止
if (_isRecording)
{
StopRecording();
}
// 确保 AudioRecord 已经正确初始化
if (_audioRecord != null && _audioRecord.State == State.Initialized)
{
Console.WriteLine("尝试开始录音");
_audioRecord.StartRecording();
_isRecording = true;
Console.WriteLine("开始录音");
VadCounter = 0; // 重置静音计数器
currentSilenceFrames = 0;
isSpeaking = false;
_recordingCts = new CancellationTokenSource();
var token = _recordingCts.Token;
Task.Run(() =>
{
byte[] buffer = new byte[BytesPerFrame];
while (_isRecording && !token.IsCancellationRequested)
{
try
{
int bytesRead = _audioRecord.Read(buffer, 0, buffer.Length);
if (bytesRead > 0)
{
// 发送音频数据
if (OnPcmAudioEvent != null)
{
OnPcmAudioEvent(buffer);
}
// VAD检测
if (useVAD)
{
bool hasVoice = DetectVoiceActivity(buffer, bytesRead);
if (hasVoice)
{
currentSilenceFrames = 0;
if (!isSpeaking)
{
// 检查是否在TTS结束后太快检测到声音可能是回音
var timeSinceTts = (DateTime.Now - lastTtsEndTime).TotalSeconds;
if (timeSinceTts < ttsCooldownTime * 2)
{
Console.WriteLine($"VAD: TTS结束后{timeSinceTts:F2}秒内检测到声音,可能是回音,忽略");
continue;
}
isSpeaking = true;
Console.WriteLine("VAD: 检测到语音开始");
}
}
else
{
currentSilenceFrames++;
// 如果之前在说话,且静音超过阈值,认为说话结束
if (isSpeaking && currentSilenceFrames > vadSilenceFrames)
{
isSpeaking = false;
VadCounter = currentSilenceFrames;
Console.WriteLine($"VAD: 检测到语音结束,静音帧数: {currentSilenceFrames}");
}
}
}
else
{
// 使用原有的简单静音检测
if (!IsAudioMute(buffer, bytesRead))
{
VadCounter = 0;
}
else
{
VadCounter++;
}
}
}
}
catch (Exception ex)
{
Console.WriteLine($"录音时出错: {ex.Message}");
}
}
}, token);
}
else
{
if (_audioRecord == null)
{
Console.WriteLine("AudioRecord 未初始化,可能是权限问题或资源冲突");
// 尝试重新初始化AudioRecord
try
{
int minBufferSize = AudioRecord.GetMinBufferSize(SampleRate_WaveIn, ChannelIn.Mono, Android.Media.Encoding.Pcm16bit);
int bufferSize = Math.Max(minBufferSize, BytesPerFrame * 2); // 至少能容纳2帧数据
_audioRecord = new AudioRecord(AudioSource.Mic, SampleRate_WaveIn, ChannelIn.Mono, Android.Media.Encoding.Pcm16bit, bufferSize);
if (_audioRecord.State == State.Initialized)
{
Console.WriteLine("成功重新初始化AudioRecord重试录音");
StartRecording();
}
else
{
Console.WriteLine("重新初始化AudioRecord失败");
}
}
catch (Exception ex)
{
Console.WriteLine($"重新初始化AudioRecord出错: {ex.Message}");
}
}
else if (_audioRecord.State != State.Initialized)
{
Console.WriteLine($"AudioRecord 状态异常: {_audioRecord.State}");
}
else if (_isRecording)
{
Console.WriteLine("正在录音中,无法再次开始");
}
Console.WriteLine("无法开始录音AudioRecord 未初始化或正在录音中");
}
}
catch (Exception ex)
{
Console.WriteLine($"启动录音时出错: {ex.Message}");
_isRecording = false;
}
}
public void StopRecording()
{
try
{
if (_audioRecord != null && _isRecording)
{
_audioRecord.Stop();
_isRecording = false;
VadCounter = 0;
currentSilenceFrames = 0;
isSpeaking = false;
// 取消录音任务
_recordingCts?.Cancel();
_recordingCts?.Dispose();
_recordingCts = null;
Console.WriteLine("结束录音");
}
else
{
// 即使没有录音在进行,也重置状态
_isRecording = false;
VadCounter = 0;
currentSilenceFrames = 0;
isSpeaking = false;
}
}
catch (Exception ex)
{
Console.WriteLine($"停止录音时出错: {ex.Message}");
// 确保状态重置,即使发生异常
_isRecording = false;
VadCounter = 0;
currentSilenceFrames = 0;
isSpeaking = false;
}
}
private bool IsAudioMute(byte[] buffer, int bytesRecorded)
{
double rms = 0;
int sampleCount = bytesRecorded / 2; // 每个样本 2 字节
for (int i = 0; i < sampleCount; i++)
{
short sample = BitConverter.ToInt16(buffer, i * 2);
rms += sample * sample;
}
rms = Math.Sqrt(rms / sampleCount);
rms /= short.MaxValue; // 归一化到 0 - 1 范围
double MuteThreshold = 0.01; // 静音阈值
return rms < MuteThreshold;
}
// 添加VAD检测方法从Unity版本移植
private bool DetectVoiceActivity(byte[] buffer, int bytesRecorded)
{
// 计算音频能量
double energy = 0;
int sampleCount = bytesRecorded / 2; // 16位音频每个样本2字节
for (int i = 0; i < sampleCount; i++)
{
short sample = BitConverter.ToInt16(buffer, i * 2);
float normalizedSample = sample / (float)short.MaxValue;
energy += normalizedSample * normalizedSample;
}
energy /= sampleCount;
// 在TTS播放结束后的一段时间内提高VAD阈值减少误触发
float currentThreshold = vadThreshold;
var timeSinceTts = (DateTime.Now - lastTtsEndTime).TotalSeconds;
if (timeSinceTts < ttsCooldownTime * 2)
{
// 在冷却期后的一段时间内,使用更高的阈值
currentThreshold = vadThreshold * 1.5f;
// 如果能量值接近阈值但未超过,记录日志但不触发
if (energy > vadThreshold && energy <= currentThreshold)
{
Console.WriteLine($"VAD: 冷却期内检测到低能量声音,能量值: {energy:F5},当前阈值: {currentThreshold:F5}");
}
}
// 判断能量是否超过阈值
bool hasVoice = energy > currentThreshold;
// 调试信息
if (hasVoice)
{
Console.WriteLine($"VAD: 检测到声音,能量值: {energy:F5},当前阈值: {currentThreshold:F5}");
}
return hasVoice;
}
public void StartPlaying()
{
try
{
if (!_isPlaying && _audioTrack != null)
{
_audioTrack.Play();
_isPlaying = true;
// 播放开始时,停止录音以避免回音
if (_isRecording)
{
StopRecording();
}
}
}
catch (Exception ex)
{
Console.WriteLine($"启动播放时出错: {ex.Message}");
}
}
public void StopPlaying()
{
try
{
if (_isPlaying && _audioTrack != null)
{
_audioTrack.Stop();
_isPlaying = false;
// 记录TTS结束时间开始冷却期
lastTtsEndTime = DateTime.Now;
isInCooldown = true;
Console.WriteLine($"TTS播放结束进入冷却期 {ttsCooldownTime} 秒");
// 启动冷却期计时器
Task.Run(async () =>
{
await Task.Delay((int)(ttsCooldownTime * 1000));
isInCooldown = false;
Console.WriteLine("冷却期结束,可以开始新一轮录音");
});
}
}
catch (Exception ex)
{
Console.WriteLine($"停止播放时出错: {ex.Message}");
}
}
public void AddOutSamples(byte[] pcmData)
{
try
{
if (_audioTrack != null)
{
if (!_isPlaying)
{
StartPlaying();
}
_audioTrack.Write(pcmData, 0, pcmData.Length);
}
}
catch (Exception ex)
{
Console.WriteLine($"添加样本数据时出错: {ex.Message}");
}
}
public void AddOutSamples(float[] pcmData)
{
throw new NotImplementedException("不支持 float 类型的 PCM 数据");
}
// 添加VAD配置方法
public void ConfigureVAD(bool enabled, float threshold, int silenceFrames, float cooldownTime)
{
useVAD = enabled;
vadThreshold = threshold;
vadSilenceFrames = silenceFrames;
ttsCooldownTime = cooldownTime;
// 重置VAD状态
currentSilenceFrames = 0;
isSpeaking = false;
isInCooldown = false;
lastTtsEndTime = DateTime.Now.AddSeconds(-10); // 设置为很久以前,确保不在冷却期
VadCounter = 0;
Console.WriteLine($"VAD配置更新: 启用={enabled}, 阈值={threshold}, 静音帧数={silenceFrames}, 冷却时间={cooldownTime}秒");
Console.WriteLine("VAD状态已重置");
}
/// <summary>
/// 重置TTS状态用于断线重连恢复
/// </summary>
public void ResetTtsState()
{
isInCooldown = false;
lastTtsEndTime = DateTime.Now.AddSeconds(-10);
Console.WriteLine("TTS状态已重置");
}
// 获取当前VAD状态
public bool IsSpeaking => isSpeaking;
public bool IsInCooldown => isInCooldown;
public int CurrentSilenceFrames => currentSilenceFrames;
public void Dispose()
{
try
{
_isPlaying = false;
_isRecording = false;
_recordingCts?.Cancel();
_recordingCts?.Dispose();
_audioRecord?.Release();
_audioRecord = null;
_mediaPlayer?.Release();
_mediaPlayer = null;
_audioTrack?.Release();
_audioTrack = null;
}
catch (Exception ex)
{
Console.WriteLine($"释放资源时出错: {ex.Message}");
}
}
}
}