commit 12cdf9559812d703931cc215eb0b65945a693445 Author: KP9lK Date: Tue May 13 21:55:57 2025 +0300 init diff --git a/App.axaml b/App.axaml new file mode 100644 index 0000000..24010e7 --- /dev/null +++ b/App.axaml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + #ffffff + #666666 + #2b2d30 + + + + #2b2d30 + #888888 + #ffffff + + + \ No newline at end of file diff --git a/App.axaml.cs b/App.axaml.cs new file mode 100644 index 0000000..34aa9f0 --- /dev/null +++ b/App.axaml.cs @@ -0,0 +1,36 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; +using ReactiveUI; +using SoundTester.Helpers; +using SoundTester.ViewModels; +using SoundTester.Views; +using Splat; + +namespace SoundTester; + +public partial class App : Application +{ + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + Bootstrapper.Configure(); + + Current.Resources.MergedDictionaries.Add((ResourceDictionary)Application.Current.Resources["Light"]); + + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + desktop.MainWindow = new MainWindow + { + DataContext = Locator.Current.GetService(), + }; + } + + base.OnFrameworkInitializationCompleted(); + } +} \ No newline at end of file diff --git a/Assets/avalonia-logo.ico b/Assets/avalonia-logo.ico new file mode 100644 index 0000000..da8d49f Binary files /dev/null and b/Assets/avalonia-logo.ico differ diff --git a/Helpers/Bootstrapper.cs b/Helpers/Bootstrapper.cs new file mode 100644 index 0000000..cda0c64 --- /dev/null +++ b/Helpers/Bootstrapper.cs @@ -0,0 +1,36 @@ +using System; +using System.ComponentModel.Design; +using Avalonia.ReactiveUI; +using Microsoft.Extensions.DependencyInjection; +using ReactiveUI; +using SoundTester.ViewModels; +using SoundTester.Views; +using SoundTesting; +using Splat; +using Splat.Microsoft.Extensions.DependencyInjection; + +namespace SoundTester.Helpers; + +public static class Bootstrapper +{ + public static void Configure() + { + var services = new ServiceCollection(); + + services.AddSingleton(); + services.AddSingleton(); + + services.UseMicrosoftDependencyResolver(); + + var provider = services.BuildServiceProvider(); + + Locator.CurrentMutable.InitializeSplat(); + Locator.CurrentMutable.InitializeReactiveUI(); + + Locator.CurrentMutable.RegisterConstant(provider); + + Locator.CurrentMutable.Register>(() => new VoiceTrackerView()); + + RxApp.MainThreadScheduler = AvaloniaScheduler.Instance; + } +} \ No newline at end of file diff --git a/Models/DevicesEnumerator.cs b/Models/DevicesEnumerator.cs new file mode 100644 index 0000000..71c1242 --- /dev/null +++ b/Models/DevicesEnumerator.cs @@ -0,0 +1,15 @@ +using NAudio.CoreAudioApi; +using ReactiveUI; + +namespace SoundTesting; + +public class DevicesEnumerator : MMDeviceEnumerator +{ + private DevicesUpdater _devicesUpdater = new DevicesUpdater(); + + public DevicesUpdater DevicesUpdater => _devicesUpdater; + public DevicesEnumerator() + { + this.RegisterEndpointNotificationCallback(_devicesUpdater); + } +} \ No newline at end of file diff --git a/Models/DevicesUpdater.cs b/Models/DevicesUpdater.cs new file mode 100644 index 0000000..279667d --- /dev/null +++ b/Models/DevicesUpdater.cs @@ -0,0 +1,65 @@ +using System.Collections.ObjectModel; +using NAudio.CoreAudioApi; +using NAudio.CoreAudioApi.Interfaces; + +namespace SoundTesting +{ + public class + DevicesUpdater : IMMNotificationClient //Кастомный интерфейс уведомлений об изменении количества и состояния подключенных аудиоустройств + { + public ObservableCollection InputDevices { get; set; } = new(); + public ObservableCollection OutputDevices { get; set; } = new(); + + public DevicesUpdater() + { + DeviceListUpdate(); + } + + private void DeviceListUpdate() //Обновление данных об устройствах + { + + InputDevices?.Clear(); + OutputDevices?.Clear(); + + MMDeviceEnumerator enumerator = new MMDeviceEnumerator(); + + var inputs = enumerator.EnumerateAudioEndPoints(DataFlow.Capture, DeviceState.Active); + foreach (var device in inputs) + { + InputDevices.Add( device.FriendlyName ); + device.Dispose(); + } + + var outputs = enumerator.EnumerateAudioEndPoints(DataFlow.Render, DeviceState.Active); + foreach (var device in outputs) + { + OutputDevices.Add( device.FriendlyName ); + device.Dispose(); + } + enumerator.Dispose(); + } + + public void OnDeviceStateChanged(string deviceId, DeviceState newState) //Изменение состояния устройства + { + } + + public void OnDeviceAdded(string deviceId) //Добавление устройства + { + DeviceListUpdate(); + } + + public void OnDeviceRemoved(string deviceId) //Удаление устройства + { + DeviceListUpdate(); + } + + public void OnDefaultDeviceChanged(DataFlow flow, Role role, string defaultDeviceId) //Изменение устройства по-умолчанию + { + DeviceListUpdate(); + } + + public void OnPropertyValueChanged(string deviceId, PropertyKey key) //Изменение свойства устройства + { + } + } +} \ No newline at end of file diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..ce6d976 --- /dev/null +++ b/Program.cs @@ -0,0 +1,23 @@ +using Avalonia; +using Avalonia.ReactiveUI; +using System; + +namespace SoundTester; + +sealed class Program +{ + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + [STAThread] + public static void Main(string[] args) => BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .WithInterFont() + .LogToTrace() + .UseReactiveUI(); +} \ No newline at end of file diff --git a/Service/VoiceTrackerService.cs b/Service/VoiceTrackerService.cs new file mode 100644 index 0000000..7e7fac5 --- /dev/null +++ b/Service/VoiceTrackerService.cs @@ -0,0 +1,60 @@ +using System; +using System.Linq; +using Avalonia.Threading; +using NAudio.CoreAudioApi; +using NAudio.Wave; +using SoundTesting; + +namespace SoundTester.Service; + +public class VoiceTrackerService +{ + private WasapiCapture _wasapiCapture; + private WasapiOut _wasapiOut; + private BufferedWaveProvider _bufferedWaveProvider; + private DevicesEnumerator _devicesEnumerator; + private float _volume = -96; + public delegate void DecibelLevelHandler(float level); + public event DecibelLevelHandler? DecibelLevelEvent; + public VoiceTrackerService(string deviceName) + { + + var en = new MMDeviceEnumerator(); //Костыль + var inD = en.EnumerateAudioEndPoints(DataFlow.Capture, DeviceState.Active).FirstOrDefault(x => x.FriendlyName == deviceName); + var outD = en.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia); + + _wasapiCapture = new WasapiCapture(inD, false, 50); + _wasapiOut = new WasapiOut(outD, AudioClientShareMode.Shared, false, 50); + _wasapiCapture.DataAvailable += OnDataAvailable; + _bufferedWaveProvider = new BufferedWaveProvider(_wasapiCapture?.WaveFormat){ DiscardOnBufferOverflow = true }; + _wasapiOut.Init(_bufferedWaveProvider); + } + + public void Record() + { + _wasapiCapture.StartRecording(); + _wasapiOut.Play(); + } + + private void OnDataAvailable(object sender, WaveInEventArgs e) + { + float[] samples = new float[e.BytesRecorded / 2]; + for (int i = 0; i < e.BytesRecorded; i += 2) + { + short sample = BitConverter.ToInt16(e.Buffer, i); + samples[i / 2] = sample / 32768f; + } + + float sum = 0; + for (int i = 0; i < samples.Length; i++) + { + sum += samples[i] * samples[i]; + } + + float rms = (float)Math.Sqrt(sum / samples.Length); + + float res = rms > 0 ? 20 * (float)Math.Log10(rms) : -96; + DecibelLevelEvent?.Invoke(res); + _bufferedWaveProvider.AddSamples(e.Buffer, 0, e.BytesRecorded); //Добавление данных для мониторинга микрофона + } +} \ No newline at end of file diff --git a/SoundTester.csproj b/SoundTester.csproj new file mode 100644 index 0000000..a9f6eb9 --- /dev/null +++ b/SoundTester.csproj @@ -0,0 +1,30 @@ + + + WinExe + net9.0 + enable + true + app.manifest + true + + + + + + + + + + + + + + None + All + + + + + + + diff --git a/ViewLocator.cs b/ViewLocator.cs new file mode 100644 index 0000000..db828c8 --- /dev/null +++ b/ViewLocator.cs @@ -0,0 +1,30 @@ +using System; +using Avalonia.Controls; +using Avalonia.Controls.Templates; +using SoundTester.ViewModels; + +namespace SoundTester; + +public class ViewLocator : IDataTemplate +{ + public Control? Build(object? param) + { + if (param is null) + return null; + + var name = param.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal); + var type = Type.GetType(name); + + if (type != null) + { + return (Control)Activator.CreateInstance(type)!; + } + + return new TextBlock { Text = "Not Found: " + name }; + } + + public bool Match(object? data) + { + return data is ViewModelBase; + } +} \ No newline at end of file diff --git a/ViewModels/MainWindowViewModel.cs b/ViewModels/MainWindowViewModel.cs new file mode 100644 index 0000000..aa55e36 --- /dev/null +++ b/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,14 @@ +namespace SoundTester.ViewModels; + +public class MainWindowViewModel : ViewModelBase +{ + public VoiceTrackerViewModel VoiceTracker { get; } + public OscillatorViewModel Oscillator { get; } + public PanningPickerViewModel PanningPicker { get; } + public MainWindowViewModel() + { + VoiceTracker = new VoiceTrackerViewModel(); + Oscillator = new OscillatorViewModel(); + PanningPicker = new PanningPickerViewModel(); + } +} \ No newline at end of file diff --git a/ViewModels/OscillatorItemViewModel.cs b/ViewModels/OscillatorItemViewModel.cs new file mode 100644 index 0000000..76e7127 --- /dev/null +++ b/ViewModels/OscillatorItemViewModel.cs @@ -0,0 +1,172 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using NAudio.CoreAudioApi; +using NAudio.Wave; +using NAudio.Wave.SampleProviders; +using ReactiveUI; + +namespace SoundTester.ViewModels; + +public class OscillatorItemViewModel : ViewModelBase //Осциллятор (генератор волны) +{ + private WasapiOut _waveOut; + private SignalGenerator _signalGenerator; + private ISampleProvider _sampleProvider; + private SignalGeneratorType _typeRef; + private int _deviceIndex; + private float _frequency = 20f; + private bool _sin = true; + private bool _square = false; + private bool _sawtooth = false; + private bool _triangle = false; + private bool _noise = false; + private bool _isPlaying = false; + private string _itemName; + private string _groupKey; + + public int DeviceIndex + { + get => _deviceIndex; + set => this.RaiseAndSetIfChanged(ref _deviceIndex, value); + } + + [Range(20, 20000)] + public float Frequency + { + get => _frequency; + set => this.RaiseAndSetIfChanged(ref _frequency, value); + } + + public bool Sin + { + get => _sin; + set => this.RaiseAndSetIfChanged(ref _sin, value); + } + + public bool Square + { + get => _square; + set => this.RaiseAndSetIfChanged(ref _square, value); + } + + public bool Sawtooth + { + get => _sawtooth; + set => this.RaiseAndSetIfChanged(ref _sawtooth, value); + } + + public bool Triangle + { + get => _triangle; + set => this.RaiseAndSetIfChanged(ref _triangle, value); + } + + public bool Noise + { + get => _noise; + set => this.RaiseAndSetIfChanged(ref _noise, value); + } + + public bool IsPlaying + { + get => _isPlaying; + set => this.RaiseAndSetIfChanged(ref _isPlaying, value); + } + + public SignalGeneratorType SGType + { + get => SGTypeSelection(); + set => this.RaiseAndSetIfChanged(ref _typeRef, value); + } + + public string ItemName + { + get => _itemName; + set => this.RaiseAndSetIfChanged(ref _itemName, value); + } + + private SignalGeneratorType SGTypeSelection() //Выбор типа волны в зависимости от булевых знаечний + { + if (Square) + return SignalGeneratorType.Square; + else if (Sawtooth) + return SignalGeneratorType.SawTooth; + else if (Triangle) + return SignalGeneratorType.Triangle; + else if (Noise) + return SignalGeneratorType.White; + + return SignalGeneratorType.Sin; + } + + public WasapiOut WaveOut + { + get => _waveOut; + set => this.RaiseAndSetIfChanged(ref _waveOut, value); + } + + public SignalGenerator SignalGenerator1 + { + get => _signalGenerator; + set => this.RaiseAndSetIfChanged(ref _signalGenerator, value); + } + + public ISampleProvider SampleProvider + { + get => _sampleProvider; + set => this.RaiseAndSetIfChanged(ref _sampleProvider, value); + } + + public string GroupKey + { + get => _groupKey; + set => this.RaiseAndSetIfChanged(ref _groupKey, value); + } + + private void WaveOutInit(string audioDevice) //Инициализация осциллятора + { + var en = new MMDeviceEnumerator(); //костыль + var outD = en.EnumerateAudioEndPoints(DataFlow.Render, DeviceState.Active).Where(x => x.FriendlyName == audioDevice).FirstOrDefault(); + + WaveOut?.Stop(); + WaveOut?.Dispose(); + WaveOut = new WasapiOut(outD,AudioClientShareMode.Shared, false, 50); + + SignalGenerator1 = new SignalGenerator(); + SignalGenerator1.Frequency = Frequency; + SignalGenerator1.Type = SGType; + SignalGenerator1.Gain = 0.5; + + SampleProvider = SignalGenerator1.ToMono(); + WaveOut.Init(SampleProvider); + } + + public OscillatorItemViewModel() //Конструктор + { + } + + public OscillatorItemViewModel(string audioDevice) //Конструктор + { + ItemName = audioDevice; + GroupKey = KeyGen(); + + this.WhenAnyValue( + x => x.Frequency, + x => x.Sin, + x => x.Square, + x => x.Sawtooth, + x => x.Triangle, + x => x.Noise) + .Subscribe(x => + { + IsPlaying = false; + WaveOutInit(audioDevice); + }); + } + + private string KeyGen() //Генерация уникального ключа для группы радиокнопок + { + return Guid.NewGuid().ToString(); + } +} \ No newline at end of file diff --git a/ViewModels/OscillatorViewModel.cs b/ViewModels/OscillatorViewModel.cs new file mode 100644 index 0000000..dbe6366 --- /dev/null +++ b/ViewModels/OscillatorViewModel.cs @@ -0,0 +1,203 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; +using System.Windows.Input; +using ReactiveUI; +using SoundTesting; +using Splat; + +namespace SoundTester.ViewModels; + +public class OscillatorViewModel : ViewModelBase //Менеджер осцилляторов (генераторов волн) +{ + private string _playButtonText = "Начать прослушивание"; + private string _selectedDevice; + private int _selectedDeviceIndex; + private ObservableCollection _devices; + private ObservableCollection _items; + private bool _isPlaying = false; + private bool _isItemSelected = false; + private bool _isEnabled = false; + private bool _isEnabledByCount = false; + private OscillatorItemViewModel _selectedItem; + private DevicesEnumerator _devicesEnumerator; + + + public string SelectedDevice + { + get => _selectedDevice; + set => this.RaiseAndSetIfChanged(ref _selectedDevice, value); + } + + public int SelectedDeviceIndex + { + get => _selectedDeviceIndex; + set => this.RaiseAndSetIfChanged(ref _selectedDeviceIndex, value); + } + + public ObservableCollection Devices + { + get => _devices; + set => this.RaiseAndSetIfChanged(ref _devices, value); + } + + public string PlayButtonText + { + get => _playButtonText; + set => this.RaiseAndSetIfChanged(ref _playButtonText, value); + } + + public bool IsPlaying + { + get => _isPlaying; + set => this.RaiseAndSetIfChanged(ref _isPlaying, value); + } + + public ObservableCollection Items + { + get => _items; + set => this.RaiseAndSetIfChanged(ref _items, value); + } + + public bool IsItemSelected + { + get => _isItemSelected; + set => this.RaiseAndSetIfChanged(ref _isItemSelected, value); + } + + public bool IsEnabled + { + get => _isEnabled; + set => this.RaiseAndSetIfChanged(ref _isEnabled, value); + } + + public bool IsEnabledByCount + { + get => _isEnabledByCount; + set => this.RaiseAndSetIfChanged(ref _isEnabledByCount, value); + } + + public OscillatorItemViewModel SelectedItem + { + get => _selectedItem; + set => this.RaiseAndSetIfChanged(ref _selectedItem, value); + } + + public ICommand PlayCommand { get; } + + public ICommand AddItemCommand { get; } + + public ICommand RemoveItemCommand { get; } + + public OscillatorViewModel(DevicesEnumerator? devicesEnumerator = null) //Конструктор + { + _devicesEnumerator = devicesEnumerator ?? Locator.Current.GetService()!; + + Devices = _devicesEnumerator!.DevicesUpdater.InputDevices; + + SelectedDevice = Devices.FirstOrDefault(); + + Items = new ObservableCollection(); + + PlayCommand = ReactiveCommand.Create(() => Play()); + AddItemCommand = ReactiveCommand.Create(() => AddItem()); + RemoveItemCommand = ReactiveCommand.Create(() => RemoveItem()); + + this.WhenAnyValue(x => x.Devices) + .WhereNotNull().Subscribe(x => + { + SelectedDevice = null!; + SelectedDevice = Devices?.FirstOrDefault(); + ReloadDevices(); + }); + + this.WhenAnyValue(x => x.Items.Count).WhereNotNull().Subscribe(x => + { + IsEnabledByCount = Items.Count > 0 ? true : false; + }); + + this.WhenAnyValue(x => x.Devices.Count).Subscribe(x => + { + foreach (var item in Items) + { + item.WaveOut.Stop(); + item.IsPlaying = false; + } + Items.Clear(); + }); + } + + private void Play() //Запуск/остановка осцилляторов + { + if (_isPlaying!) + { + PlayButtonText = "Начать прослушивание"; + foreach (var item in _items) + { + item.WaveOut.Stop(); + item.IsPlaying = false; + } + } + else + { + PlayButtonText = "Остановить прослушивание"; + foreach (var item in _items) + { + item.WaveOut.Play(); + item.IsPlaying = true; + } + } + _isPlaying = !_isPlaying; + } + + private void AddItem() //Добавление нового осциллятора + { + if (Items.Count < 10) + { + foreach (var item in Items) + { + item.WaveOut.Stop(); + item.IsPlaying = false; + } + + IsPlaying = false; + PlayButtonText = "Начать прослушивание"; + + OscillatorItemViewModel oscillatorItem = new OscillatorItemViewModel(SelectedDevice); + Items.Add(oscillatorItem); + + foreach (var ite in Items) + { + ite.WhenAnyValue( + x => x.Frequency, + x => x.Sin, + x => x.Square, + x => x.Triangle, + x => x.Sawtooth, + x => x.Noise).Subscribe(x => + { + ite.WaveOut?.Stop(); + ite.IsPlaying = false; + PlayButtonText = "Начать прослушивание"; + }); + } + } + + } + + private void RemoveItem() //Удаление выбранного осциллятора + { + if (SelectedItem != null) + { + SelectedItem.WaveOut.Stop(); + Items.Remove(SelectedItem); + } + } + + private void ReloadDevices() => this.WhenAnyValue(x => x.SelectedDevice) + .Subscribe(_ => + { + Devices = _devicesEnumerator.DevicesUpdater.OutputDevices; + IsEnabled = SelectedDeviceIndex == -1 || Devices.Count == 0 ? false : true; + }); +} \ No newline at end of file diff --git a/ViewModels/PanningPickerViewModel.cs b/ViewModels/PanningPickerViewModel.cs new file mode 100644 index 0000000..7eac7fd --- /dev/null +++ b/ViewModels/PanningPickerViewModel.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; +using System.Windows.Input; +using NAudio.CoreAudioApi; +using NAudio.Wave; +using NAudio.Wave.SampleProviders; +using ReactiveUI; +using SoundTesting; +using Splat; + +namespace SoundTester.ViewModels; + +public class PanningPickerViewModel : ViewModelBase //Тестер панорамирования +{ + private string _playButtonContent = "Начать проверку"; + private string _selectedDevice; + private ObservableCollection _devices; + private float _panningValue = 0f; + private int _selectedIndex; + private bool _isPlaying; + private bool _isEnabled = false; + private WasapiOut _wasapiOut; + + private SignalGenerator _signalGenerator = new SignalGenerator + { + Gain = 0.5, + Frequency = 440, + Type = SignalGeneratorType.Sin + }; + private ISampleProvider _sampleProvider; + + private DevicesEnumerator _devicesEnumerator; + + public string PlayButtonContent + { + get => _playButtonContent; + set => this.RaiseAndSetIfChanged(ref _playButtonContent, value); + } + + public float PanningValue + { + get => _panningValue; + set => this.RaiseAndSetIfChanged(ref _panningValue, value); + } + + public int SelectedIndex + { + get => _selectedIndex; + set => this.RaiseAndSetIfChanged(ref _selectedIndex, value); + } + + public string SelectedDevice + { + get => _selectedDevice; + set => this.RaiseAndSetIfChanged(ref _selectedDevice, value); + } + + public ObservableCollection Devices + { + get => _devices; + set => this.RaiseAndSetIfChanged(ref _devices, value); + } + + public bool IsPlaying + { + get => _isPlaying; + set => this.RaiseAndSetIfChanged(ref _isPlaying, value); + } + + public bool IsEnabled + { + get => _isEnabled; + set => this.RaiseAndSetIfChanged(ref _isEnabled, value); + } + + public ICommand PlayCommand { get; } + + public PanningPickerViewModel(DevicesEnumerator? devicesEnumerator = null) //конструктор + { + _devicesEnumerator = devicesEnumerator ?? Locator.Current.GetService()!; + + Devices = _devicesEnumerator!.DevicesUpdater.OutputDevices; + IsEnabled = SelectedIndex == -1 || Devices.Count == 0 ? false : true; + + SelectedDevice = Devices.FirstOrDefault(); + + PlayCommand = ReactiveCommand.Create( () => Play()); + + this.WhenAnyValue(x => x.Devices, x => x.SelectedDevice) + .WhereNotNull() + .Subscribe(x => + { + Devices = _devicesEnumerator.DevicesUpdater.OutputDevices; + IsEnabled = SelectedIndex == -1 || Devices.Count == 0 ? false : true; + }); + + this.WhenAnyValue(x => x.PanningValue, x => x.SelectedIndex).Subscribe(x => + { + if (IsPlaying) + { + IsPlaying = false; + + _wasapiOut?.Stop(); + + PlayButtonContent = "Начать проверку"; + + PanningAudioInit(); + } + }); + } + + private void Play() //Запуск/остановка тестера + { + if (_isPlaying) + { + _wasapiOut?.Stop(); + + PlayButtonContent = "Начать проверку"; + } + else + { + PanningAudioInit(); + + _wasapiOut.Play(); + + PlayButtonContent = "Остановить проверку"; + } + _isPlaying = !_isPlaying; + } + + private void PanningAudioInit() //Инициализация тестера + { + _wasapiOut = null; + + var en = new MMDeviceEnumerator(); //костыль + var outD = en.EnumerateAudioEndPoints(DataFlow.Render, DeviceState.Active).Where(x => x.FriendlyName == SelectedDevice).FirstOrDefault(); + + _wasapiOut = new WasapiOut(outD,AudioClientShareMode.Shared, false, 50); //Инициализация WasapiOut с помощью SelectedDevice не работает т.к. не удается корректно привести объект к интерфейсу IMMDevice + + _sampleProvider = _signalGenerator.ToMono(); + PanningSampleProvider panning = new PanningSampleProvider(_sampleProvider); + panning.Pan = PanningValue; + _wasapiOut.Init(panning); + + en.Dispose(); + } +} \ No newline at end of file diff --git a/ViewModels/ViewModelBase.cs b/ViewModels/ViewModelBase.cs new file mode 100644 index 0000000..fea5152 --- /dev/null +++ b/ViewModels/ViewModelBase.cs @@ -0,0 +1,7 @@ +using ReactiveUI; + +namespace SoundTester.ViewModels; + +public class ViewModelBase : ReactiveObject +{ +} \ No newline at end of file diff --git a/ViewModels/VoiceTrackerViewModel.cs b/ViewModels/VoiceTrackerViewModel.cs new file mode 100644 index 0000000..cf9865c --- /dev/null +++ b/ViewModels/VoiceTrackerViewModel.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; +using System.Windows.Input; +using Avalonia.Threading; +using NAudio.CoreAudioApi; +using NAudio.Wave; +using ReactiveUI; +using SoundTester.Service; +using SoundTesting; +using Splat; + +namespace SoundTester.ViewModels; + +public class VoiceTrackerViewModel : ViewModelBase +{ + private string _recButtonContent = "Начать проверку"; + private string _selectedDevice; + private int _selectedDeviceIndex; + private ObservableCollection _devices; + private float _volume = -96; + private bool _isMonitoring = false; + private bool _isEnabled = false; + private WasapiCapture _wasapiCapture; + private WasapiOut _wasapiOut; + private BufferedWaveProvider _bufferedWaveProvider; + private DevicesEnumerator _devicesEnumerator; + + + public string SelectedDevice + { + get => _selectedDevice; + set => this.RaiseAndSetIfChanged(ref _selectedDevice, value); + } + + public int SelectedDeviceIndex + { + get => _selectedDeviceIndex; + set => this.RaiseAndSetIfChanged(ref _selectedDeviceIndex, value); + } + + public ObservableCollection? Devices + { + get => _devices; + set => this.RaiseAndSetIfChanged(ref _devices, value); + } + + public float Volume + { + get => _volume; + set => this.RaiseAndSetIfChanged(ref _volume, value); + } + + public bool IsEnabled + { + get => _isEnabled; + set => this.RaiseAndSetIfChanged(ref _isEnabled, value); + } + + public string RecButtonContent + { + get => _recButtonContent; + set => this.RaiseAndSetIfChanged(ref _recButtonContent, value); + } + + public ICommand RecordCommand { get; } + + public VoiceTrackerViewModel(DevicesEnumerator? devicesEnumerator = null) //Конструктор + { + ; + + _devicesEnumerator = devicesEnumerator ?? Locator.Current.GetService()!; + + Devices = _devicesEnumerator!.DevicesUpdater.InputDevices; + SelectedDevice = Devices.FirstOrDefault(); + VoiceTrackerService voiceTrackerService = new VoiceTrackerService(SelectedDevice); + voiceTrackerService.DecibelLevelEvent += DecibelTracker; + RecordCommand = ReactiveCommand.Create(() => voiceTrackerService.Record()); + this.WhenAnyValue(x => x.Devices, x => x.SelectedDevice) + .WhereNotNull() + .Subscribe(x => + { + Devices = _devicesEnumerator.DevicesUpdater.InputDevices; + IsEnabled = SelectedDeviceIndex == -1 || Devices.Count == 0 ? false : true; + }); + + + } + + private void DecibelTracker(float level) + { + RefreshProgressBar(level); + } + + + + private void RefreshProgressBar(float percentage) + { + Volume = percentage; + } +} \ No newline at end of file diff --git a/ViewModels/VolumePickerViewModel.cs b/ViewModels/VolumePickerViewModel.cs new file mode 100644 index 0000000..213657d --- /dev/null +++ b/ViewModels/VolumePickerViewModel.cs @@ -0,0 +1,6 @@ +namespace SoundTester.ViewModels; + +public class VolumePickerViewModel +{ + +} \ No newline at end of file diff --git a/Views/MainWindow.axaml b/Views/MainWindow.axaml new file mode 100644 index 0000000..7b30a4d --- /dev/null +++ b/Views/MainWindow.axaml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Views/MainWindow.axaml.cs b/Views/MainWindow.axaml.cs new file mode 100644 index 0000000..748c2be --- /dev/null +++ b/Views/MainWindow.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.ReactiveUI; +using SoundTester.ViewModels; + +namespace SoundTester.Views; + +public partial class MainWindow : ReactiveWindow +{ + public MainWindow() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/Views/OscillatorItemView.axaml b/Views/OscillatorItemView.axaml new file mode 100644 index 0000000..a5190cb --- /dev/null +++ b/Views/OscillatorItemView.axaml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Views/OscillatorItemView.axaml.cs b/Views/OscillatorItemView.axaml.cs new file mode 100644 index 0000000..6c3b3d9 --- /dev/null +++ b/Views/OscillatorItemView.axaml.cs @@ -0,0 +1,16 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; +using SoundTester.ViewModels; + +namespace SoundTester.Views; + +public partial class OscillatorItemView : ReactiveUserControl +{ + public OscillatorItemView() + { + + InitializeComponent(); + } +} \ No newline at end of file diff --git a/Views/OscillatorView.axaml b/Views/OscillatorView.axaml new file mode 100644 index 0000000..b1bf839 --- /dev/null +++ b/Views/OscillatorView.axaml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + +