Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 31 additions & 27 deletions Controls/ComponentSettings/DecibelComponentSettingsControl.axaml
Original file line number Diff line number Diff line change
@@ -1,17 +1,3 @@
<!--
<ci:ComponentBase> 是一个用户控件基类,用于定义组件设置界面。

参数:
x:Class - 指定该控件的完整类型名称,包括命名空间。
此处为 "Decibel_Monitor.Controls.ComponentSettings.DecibelComponentSettingsControl"
x:TypeArguments - 泛型参数,指定绑定的数据模型类型。
这里是 componentSettingsModel:DecibelComponentSettings

功能:
提供一个可滚动的设置面板,允许用户配置与分贝显示相关的组件设置,
包括图片路径和圆角半径两个选项。
-->

<ci:ComponentBase
x:Class="Decibel_Monitor.Controls.ComponentSettings.DecibelComponentSettingsControl"
x:TypeArguments="componentSettingsModel:DecibelComponentSettings"
Expand All @@ -20,31 +6,49 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ci="http://classisland.tech/schemas/xaml/core"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:local="clr-namespace:Decibel_Monitor.Controls.ComponentSettings"
xmlns:componentSettingsModel="clr-namespace:Decibel_Monitor.Models.ComponentSettings"
mc:Ignorable="d">

<!-- 绑定到当前设置控件实例,便于访问 ReferenceDecibel 与 Settings -->
<!-- 本面板("小组件显示侧")仅包含该组件的显示偏好。
测量/校准(参考 dB、放大倍数)为全局设置,请到 设置 → Decibel_Monitor 页 校准;
提醒相关设置请到 设置 → 通知 → 分贝提醒。 -->
<ScrollViewer
DataContext="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:DecibelComponentSettingsControl}}}">
<StackPanel Classes="settings-container animated-intro" Margin="12" Spacing="8">
<TextBlock FontSize="14" FontWeight="Bold" Text="校准(Calibration)" Margin="0,0,0,4"/>

<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Spacing="8">
<TextBlock VerticalAlignment="Center" Width="88" Text="参考(dB)" />
<!-- 用户可输入物理/显示 dB(0~150),内部会转换为 dBFS -->
<Slider Width="260" Minimum="0" Maximum="150" Value="{Binding ReferenceDecibel, Mode=TwoWay}" TickFrequency="1" />
<TextBlock VerticalAlignment="Center" Width="64" Text="{Binding ReferenceDecibel, StringFormat='{}{0:F1} dB'}" />
<TextBlock VerticalAlignment="Center" Width="88" Text="组件内提示" />
<ToggleSwitch IsChecked="{Binding Settings.ShowAlertTextOnComponent, Mode=TwoWay}" />
</StackPanel>

<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" Spacing="8">
<Button Click="On_Calibrate" Content="校准" Padding="8,6" />
<TextBlock VerticalAlignment="Center" Text="当前放大倍数:" />
<TextBlock VerticalAlignment="Center" Text="{Binding Settings.Magnification, Mode=OneWay, StringFormat='{}{0:F3}×'}" />
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Spacing="8">
<TextBlock VerticalAlignment="Center" Width="88" Text="分贝: 前缀" />
<ToggleSwitch IsChecked="{Binding Settings.ShowDecibelPrefix, Mode=TwoWay}" />
</StackPanel>

<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Spacing="8">
<TextBlock VerticalAlignment="Center" Width="88" Text="更新频率" />
<Slider Width="200" Minimum="100" Maximum="1000" TickFrequency="50"
Value="{Binding Settings.UpdateIntervalMs, Mode=TwoWay}"
IsSnapToTickEnabled="True" />
<TextBlock VerticalAlignment="Center" Width="110"
Text="{Binding Settings.UpdateIntervalMs, StringFormat='{}{0} ms'}" />
</StackPanel>

<TextBlock FontSize="12" Classes="muted" Text="说明:滑块为 dB(0..150)。校准时会把该 dB 转换为 dBFS(dBFS = dB - 150),再用于放大倍数计算。" TextWrapping="Wrap"/>
<TextBlock FontSize="12" Classes="muted"
Text="显示设置:开启“分贝: 前缀”时数值显示为“分贝: 70.0”形式;“更新频率”越小数值响应越快(建议 100–500 ms)。"
TextWrapping="Wrap"/>

<TextBlock FontSize="12" Classes="muted"
Text="开启“组件内提示”后,当分贝超过通知设置中的阈值时,会在本组件上显示红色提示文字(如“请保持安静”);关闭后仍会发送系统通知,但不再在组件上显示提示文字。"
TextWrapping="Wrap"/>

<TextBlock FontSize="12" Classes="muted"
Text="分贝数值由全局校准决定(同一麦克风所有组件显示一致)。校准与放大倍数请到 设置 → Decibel_Monitor 页 操作。"
TextWrapping="Wrap"/>
</StackPanel>
</ScrollViewer>
</ci:ComponentBase>
</ci:ComponentBase>


249 changes: 9 additions & 240 deletions Controls/ComponentSettings/DecibelComponentSettingsControl.axaml.cs
Original file line number Diff line number Diff line change
@@ -1,258 +1,27 @@
using System;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using System;
using ClassIsland.Core.Abstractions.Controls;
using ClassIsland.Core.Controls;
using Decibel_Monitor.Models.ComponentSettings;
using NAudio.CoreAudioApi;
using NAudio.Wave;

namespace Decibel_Monitor.Controls.ComponentSettings;

// <summary>
// 分贝组件设置控件类,用于管理分贝监测组件的相关配置界面
// 继承自ComponentBase基类,使用DecibelComponentSettings作为泛型参数
// </summary>
public partial class DecibelComponentSettingsControl : ComponentBase<DecibelComponentSettings>, INotifyPropertyChanged, IDisposable
/// <summary>
/// 分贝组件设置控件("小组件显示侧")。
/// 仅包含该组件的显示偏好;测量/校准(全局)与提醒设置分别位于插件设置页与通知提供方设置。
/// </summary>
public partial class DecibelComponentSettingsControl : ComponentBase<DecibelComponentSettings>, IDisposable
{
private readonly MMDeviceEnumerator _enumerator;
private bool _disposed;

// 默认改为 -80 dBFS 对应显示 dB 为 70(因为 dB = dBFS + 150),但这里保留为 -80 dBFS 的语义先前使用的默认
// 当前 ReferenceDecibel 表示 UI 上的 dB(0..150)
private double _referenceDecibel = 70.0; // 显示层面的默认 dB(可调整为合适值)
public event PropertyChangedEventHandler? PropertyChanged;

public double ReferenceDecibel
{
get => _referenceDecibel;
set
{
if (Math.Abs(_referenceDecibel - value) < 0.0001) return;
_referenceDecibel = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ReferenceDecibel)));
}
}

public DecibelComponentSettingsControl()
{
InitializeComponent();
_enumerator = new MMDeviceEnumerator();
}

// 可选:列出系统捕获设备用于诊断(调试时调用)
private void ShowAvailableCaptureDevices()
{
try
{
var devices = _enumerator.EnumerateAudioEndPoints(DataFlow.Capture, DeviceState.Active)
.Select(d => $"{d.FriendlyName} ({d.ID})");
var text = string.Join("\n", devices);
CommonTaskDialogs.ShowDialog("捕获设备列表", text);
}
catch
{
/* 忽略诊断失败 */
}
}

// 异步版:返回当前默认音频输入设备的线性峰值(0..1)
// 增加多重回退:MMDevice.AudioMeterInformation -> WasapiCapture(device) -> WasapiCapture() -> WaveInEvent
public async Task<float> GetVoicePeakLinearAsync()
{
try
{
var captureDevices = _enumerator.EnumerateAudioEndPoints(DataFlow.Capture, DeviceState.Active).ToArray();
var defaultDevice = _enumerator.GetDefaultAudioEndpoint(DataFlow.Capture, Role.Console);
var selected = captureDevices.FirstOrDefault(c => c.ID == defaultDevice.ID) ?? defaultDevice;

// 1) 先读 AudioMeterInformation(低成本)
var linearValue = selected?.AudioMeterInformation?.MasterPeakValue ?? 0f;
if (linearValue > 0.0001f)
{
return linearValue;
}

// 2) 尝试 WasapiCapture 指定设备
try
{
var v = await GetVoicePeakLinearByWasapiCaptureAsync(selected, 600).ConfigureAwait(false);
if (v > 0.0001f) return v;
}
catch { /* 忽略,继续回退 */ }

// 3) 尝试 WasapiCapture 默认设备(无 device 参数)
try
{
var v = await GetVoicePeakLinearByWasapiCaptureAsync(null, 600).ConfigureAwait(false);
if (v > 0.0001f) return v;
}
catch { /* 忽略 */ }

// 4) 尝试 WaveInEvent(旧 API,兼容性更好)
try
{
var v = await GetVoicePeakLinearByWaveInAsync(600).ConfigureAwait(false);
if (v > 0.0001f) return v;
}
catch { /* 忽略 */ }

return 0f;
}
catch
{
return 0f;
}
}

// 使用 WasapiCapture(可传入 MMDevice 或 null 表示默认设备)
private async Task<float> GetVoicePeakLinearByWasapiCaptureAsync(MMDevice? device, int captureMs = 600)
{
try
{
float maxSample = 0f;
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);

using var capture = device is null ? new WasapiCapture() : new WasapiCapture(device);

capture.DataAvailable += (s, e) =>
{
try
{
var wf = capture.WaveFormat;
if (wf.Encoding == WaveFormatEncoding.IeeeFloat)
{
for (int n = 0; n + 4 <= e.BytesRecorded; n += 4)
{
float sample = BitConverter.ToSingle(e.Buffer, n);
maxSample = Math.Max(maxSample, Math.Abs(sample));
}
}
else if (wf.BitsPerSample == 16)
{
for (int n = 0; n + 2 <= e.BytesRecorded; n += 2)
{
short s16 = BitConverter.ToInt16(e.Buffer, n);
float sample = Math.Abs(s16 / 32768f);
maxSample = Math.Max(maxSample, sample);
}
}
else if (wf.BitsPerSample == 32)
{
for (int n = 0; n + 4 <= e.BytesRecorded; n += 4)
{
int i32 = BitConverter.ToInt32(e.Buffer, n);
float sample = Math.Abs(i32 / (float)int.MaxValue);
maxSample = Math.Max(maxSample, sample);
}
}
}
catch
{
}
};

capture.RecordingStopped += (s, e) => tcs.TrySetResult(true);

capture.StartRecording();
await Task.Delay(captureMs).ConfigureAwait(false);
capture.StopRecording();
await Task.WhenAny(tcs.Task, Task.Delay(1000)).ConfigureAwait(false);

return Math.Clamp(maxSample, 0f, 1f);
}
catch
{
return 0f;
}
}

// 使用 WaveInEvent 作为最后回退(与系统默认设备交互良好)
private async Task<float> GetVoicePeakLinearByWaveInAsync(int captureMs = 600)
{
try
{
float maxSample = 0f;
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);

using var waveIn = new WaveInEvent();
waveIn.WaveFormat = new WaveFormat(16000, 16, 1); // 单声道 16k 16bit
waveIn.DataAvailable += (s, e) =>
{
try
{
for (int i = 0; i + 2 <= e.BytesRecorded; i += 2)
{
short s16 = BitConverter.ToInt16(e.Buffer, i);
float sample = Math.Abs(s16 / 32768f);
maxSample = Math.Max(maxSample, sample);
}
}
catch { }
};
waveIn.RecordingStopped += (s, e) => tcs.TrySetResult(true);

waveIn.StartRecording();
await Task.Delay(captureMs).ConfigureAwait(false);
waveIn.StopRecording();
await Task.WhenAny(tcs.Task, Task.Delay(1000)).ConfigureAwait(false);

return Math.Clamp(maxSample, 0f, 1f);
}
catch
{
return 0f;
}
}

// 异步事件处理:避免阻塞 UI 线程
public async void On_Calibrate(object? sender, RoutedEventArgs e)
{
try
{
float measuredLinear = await GetVoicePeakLinearAsync().ConfigureAwait(false);

await Dispatcher.UIThread.InvokeAsync(() =>
{
if (measuredLinear <= 0f)
{
try { CommonTaskDialogs.ShowDialog("校准结果", "未检测到有效输入,无法完成校准。请确认麦克风已启用并有声源。"); } catch { }
return;
}

// ReferenceDecibel 为 UI 上的 dB(0..150)
double targetDb = ReferenceDecibel;
// 将显示 dB 转换为 dBFS(与组件显示一致):dBFS = dB - 150
double targetDbFS = targetDb - 150.0;

double targetLinear = Math.Pow(10.0, targetDbFS / 20.0);
double magnification = targetLinear / measuredLinear;
// 限制放大倍数上限为 1024,下限 0
magnification = Math.Clamp(magnification, 0.0, 1024.0);

double adjustedLinear = measuredLinear * magnification;
double adjustedDb = adjustedLinear > 0 ? 20.0 * Math.Log10(adjustedLinear) : double.NegativeInfinity;
double measuredDb = measuredLinear > 0 ? 20.0 * Math.Log10(measuredLinear) : double.NegativeInfinity;

if (Settings is not null)
{
Settings.Magnification = magnification;
try { CommonTaskDialogs.ShowDialog("校准结果", $"测量线性值: {measuredLinear:F4}\n" + $"测量 dBFS: {measuredDb:F1} dB\n" + $"目标 dB (显示): {targetDb:F1} dB\n" + $"目标 dBFS: {targetDbFS:F1} dBFS\n" + $"放大倍数: {magnification:F6}×\n" + $"放大后 dBFS (验证): {adjustedDb:F1} dB"); } catch { }
}
else { try { CommonTaskDialogs.ShowDialog("校准完成(未保存)", $"测量线性值: {measuredLinear:F4}\n" + $"测量 dBFS: {measuredDb:F1} dB\n" + $"目标 dB (显示): {targetDb:F1} dB\n" + $"目标 dBFS: {targetDbFS:F1} dBFS\n" + $"建议放大倍数: {magnification:F6}×\n" + $"放大后 dBFS (验证): {adjustedDb:F1} dB\n\n" + "当前控件未绑定到组件实例,无法直接保存设置。"); } catch { } }
});
}
catch (Exception ex) { try { CommonTaskDialogs.ShowDialog("校准失败", $"发生异常:{ex.Message}"); } catch { } }
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
_enumerator.Dispose();
}
}
}


Loading