using System; using System.IO; using System.Text.Json; using System.Threading; using System.Collections.Concurrent; using Vosk; using SoundIOSharp; using Avalonia.Threading; namespace VisionAsist.Models; public class TrigerCore : IDisposable { private SoundIO? _soundIO; private SoundIODevice? _device; private SoundIOInStream? _inStream; private Thread? _eventThread; private Thread? _processingThread; private bool _isRecording; private readonly Model _model; private readonly VoskRecognizer _rec; private readonly BlockingCollection _audioQueue = new(100); public string RecognizedText { get; private set; } = ""; public event Action? OnRecognized; // Сохраняем ссылки в полях класса, чтобы GC не удалил их в Debug-режиме private Action? _readCallback; private Action? _overflowCallback; private Action? _errorCallback; public TrigerCore() { string voskPath = Path.Combine(AppContext.BaseDirectory, "models", "Vosk"); if (!Directory.Exists(voskPath)) throw new DirectoryNotFoundException($"Модель Vosk не найдена: {voskPath}"); _model = new Model(voskPath); _rec = new VoskRecognizer(_model, 16000.0f); } public void StartRecording() { if (_isRecording) return; _soundIO = new SoundIO(); _soundIO.Connect(); _soundIO.FlushEvents(); int deviceIndex = _soundIO.DefaultInputDeviceIndex; if (deviceIndex < 0) throw new Exception("Микрофон не найден."); _device = _soundIO.GetInputDevice(deviceIndex); _inStream = _device.CreateInStream(); _inStream.Format = SoundIOFormat.S16LE; _inStream.SampleRate = 16000; _inStream.Layout = SoundIOChannelLayout.GetDefault(1); // Привязываем методы к полям _readCallback = OnDataAvailable; _overflowCallback = () => Console.WriteLine("Audio Buffer Overflow"); _errorCallback = () => Console.WriteLine("Audio Stream Error occurred"); _inStream.ReadCallback = _readCallback; _inStream.OverflowCallback = _overflowCallback; _inStream.ErrorCallback = _errorCallback; _inStream.Open(); _inStream.Start(); _isRecording = true; // Поток обработки событий SoundIO _eventThread = new Thread(() => { while (_isRecording && _soundIO != null) { try { _soundIO.WaitEvents(); } catch { break; } } }) { IsBackground = true, Name = "SoundIO_Wait" }; _eventThread.Start(); // Поток обработки Vosk _processingThread = new Thread(ProcessQueue) { IsBackground = true, Name = "Vosk_Worker" }; _processingThread.Start(); } private unsafe void OnDataAvailable(int frameCountMin, int frameCountMax) { if (!_isRecording || _inStream == null) return; int frameCount = frameCountMax; // Работаем с результатом BeginRead как с объектом SoundIOChannelAreas var areas = _inStream.BeginRead(ref frameCount); if (frameCount <= 0) return; try { var area = areas.GetArea(0); if (area.Pointer == IntPtr.Zero) return; int bytesNeeded = frameCount * 2; // 2 байта на семпл для S16LE byte[] data = new byte[bytesNeeded]; fixed (byte* pDst = data) { short* pSrc = (short*)area.Pointer; short* pDstShort = (short*)pDst; int stepInShorts = area.Step / 2; for (int i = 0; i < frameCount; i++) { pDstShort[i] = pSrc[i * stepInShorts]; } } // Отправляем в очередь и мгновенно освобождаем аудио-поток _audioQueue.TryAdd(data); } catch (Exception ex) { Console.WriteLine("Error copying audio data: " + ex.Message); } finally { _inStream.EndRead(); } } private void ProcessQueue() { // GetConsumingEnumerable будет ждать появления данных в очереди foreach (var data in _audioQueue.GetConsumingEnumerable()) { if (!_isRecording) break; try { bool isFinal; lock (_rec) { isFinal = _rec.AcceptWaveform(data, data.Length); } if (isFinal) { ParseAndSend(_rec.Result()); } } catch (Exception ex) { Console.WriteLine("Vosk processing error: " + ex.Message); } } } private void ParseAndSend(string json) { if (string.IsNullOrWhiteSpace(json)) return; try { using var doc = JsonDocument.Parse(json); if (doc.RootElement.TryGetProperty("text", out var el)) { string text = el.GetString() ?? ""; if (!string.IsNullOrWhiteSpace(text)) { RecognizedText += text + " "; // Безопасный проброс в UI поток Avalonia Dispatcher.UIThread.Post(() => OnRecognized?.Invoke(text)); } } } catch { /* JSON Parse Error */ } } public void StopRecording() { if (!_isRecording) return; _isRecording = false; _audioQueue.CompleteAdding(); _soundIO?.Wakeup(); _inStream?.Dispose(); _device?.RemoveReference(); _soundIO?.Dispose(); _inStream = null; _device = null; _soundIO = null; } public void Dispose() { StopRecording(); _rec?.Dispose(); _model?.Dispose(); } }