Move WPF example to MVVM

This commit is contained in:
Carlos Fonseca M 2019-11-13 04:25:05 -06:00
parent b70de5a9ba
commit 42e533016c
7 changed files with 558 additions and 315 deletions

View File

@ -1,9 +1,7 @@
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
using CommonServiceLocator;
using DeepSpeech.WPF.ViewModels;
using DeepSpeechClient.Interfaces;
using GalaSoft.MvvmLight.Ioc;
using System.Windows;
namespace DeepSpeechWPF
@ -13,5 +11,34 @@ namespace DeepSpeechWPF
/// </summary>
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
const int BEAM_WIDTH = 500;
//Register instance of DeepSpeech
DeepSpeechClient.DeepSpeech deepSpeechClient = new DeepSpeechClient.DeepSpeech();
try
{
deepSpeechClient.CreateModel("output_graph.pbmm", BEAM_WIDTH);
}
catch (System.Exception ex)
{
MessageBox.Show(ex.Message);
Current.Shutdown();
}
SimpleIoc.Default.Register<IDeepSpeech>(() => deepSpeechClient);
SimpleIoc.Default.Register<MainWindowViewModel>();
}
protected override void OnExit(ExitEventArgs e)
{
base.OnExit(e);
//Dispose instance of DeepSpeech
ServiceLocator.Current.GetInstance<IDeepSpeech>()?.Dispose();
}
}
}

View File

@ -40,15 +40,36 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<Reference Include="AsyncAwaitBestPractices, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>packages\AsyncAwaitBestPractices.3.1.0\lib\netstandard1.0\AsyncAwaitBestPractices.dll</HintPath>
</Reference>
<Reference Include="AsyncAwaitBestPractices.MVVM, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>packages\AsyncAwaitBestPractices.MVVM.3.1.0\lib\netstandard1.0\AsyncAwaitBestPractices.MVVM.dll</HintPath>
</Reference>
<Reference Include="CommonServiceLocator, Version=2.0.2.0, Culture=neutral, PublicKeyToken=489b6accfaf20ef0, processorArchitecture=MSIL">
<HintPath>packages\CommonServiceLocator.2.0.2\lib\net45\CommonServiceLocator.dll</HintPath>
</Reference>
<Reference Include="CSCore, Version=1.2.1.2, Culture=neutral, PublicKeyToken=5a08f2b6f4415dea, processorArchitecture=MSIL">
<HintPath>packages\CSCore.1.2.1.2\lib\net35-client\CSCore.dll</HintPath>
</Reference>
<Reference Include="GalaSoft.MvvmLight, Version=5.4.1.0, Culture=neutral, PublicKeyToken=e7570ab207bcb616, processorArchitecture=MSIL">
<HintPath>packages\MvvmLightLibs.5.4.1.1\lib\net45\GalaSoft.MvvmLight.dll</HintPath>
</Reference>
<Reference Include="GalaSoft.MvvmLight.Extras, Version=5.4.1.0, Culture=neutral, PublicKeyToken=669f0b5e8f868abf, processorArchitecture=MSIL">
<HintPath>packages\MvvmLightLibs.5.4.1.1\lib\net45\GalaSoft.MvvmLight.Extras.dll</HintPath>
</Reference>
<Reference Include="GalaSoft.MvvmLight.Platform, Version=5.4.1.0, Culture=neutral, PublicKeyToken=5f873c45e98af8a1, processorArchitecture=MSIL">
<HintPath>packages\MvvmLightLibs.5.4.1.1\lib\net45\GalaSoft.MvvmLight.Platform.dll</HintPath>
</Reference>
<Reference Include="NAudio, Version=1.9.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>packages\NAudio.1.9.0\lib\net35\NAudio.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Data" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Windows.Interactivity, Version=4.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>packages\MvvmLightLibs.5.4.1.1\lib\net45\System.Windows.Interactivity.dll</HintPath>
</Reference>
<Reference Include="System.Xml" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Core" />
@ -67,6 +88,7 @@
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</ApplicationDefinition>
<Compile Include="ViewModels\MainWindowViewModel.cs" />
<Page Include="MainWindow.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
@ -75,6 +97,7 @@
<DependentUpon>App.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Include="ViewModels\BindableBase.cs" />
<Compile Include="MainWindow.xaml.cs">
<DependentUpon>MainWindow.xaml</DependentUpon>
<SubType>Code</SubType>

View File

@ -16,11 +16,10 @@
<RowDefinition />
</Grid.RowDefinitions>
<TextBox
x:Name="txtResult"
Grid.Row="1"
Margin="10,36,10,10"
FontSize="16px"
Text=""
Text="{Binding Transcription, Mode=OneWay}"
TextWrapping="Wrap" />
<Label
Grid.Row="1"
@ -34,78 +33,70 @@
VerticalAlignment="Top"
Content="Select an audio file to transcript:" />
<TextBox
x:Name="txtFileName"
Height="23"
Margin="10,41,10,0"
VerticalAlignment="Top"
Text=""
Text="{Binding AudioFilePath, Mode=TwoWay}"
TextWrapping="Wrap" />
<Button
x:Name="btnOpenFile"
Width="80"
Height="25"
Margin="10,69,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Click="BtnOpenFile_Click"
Content="Open file"
IsEnabled="False" />
Command="{Binding SelectFileCommand}"
Content="Open file" />
<Button
x:Name="btnEnableLM"
Width="82"
Height="25"
Margin="95,69,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Click="BtnEnableLM_Click"
Content="Enable LM"
IsEnabled="False" />
Command="{Binding EnableLanguageModelCommand}"
Content="Enable LM" />
<Button
x:Name="btnTranscript"
Width="75"
Height="25"
Margin="182,69,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Click="BtnTranscript_Click"
Content="Transcript"
IsEnabled="False" />
Command="{Binding InferenceFromFileCommand}"
Content="Transcript" />
<Label
x:Name="lblStatus"
Height="30"
Margin="10,192,10,0"
Margin="10,99,10,0"
VerticalAlignment="Top"
Content="" />
Content="{Binding StatusMessage, Mode=OneWay}" />
<Label
Height="26"
Margin="10,119,10,0"
Margin="10,158,10,0"
VerticalAlignment="Top"
Content="Select an audio input:" />
<ComboBox
x:Name="cbxAudioInputs"
Height="23"
Margin="20,150,186,0"
Margin="20,189,186,0"
VerticalAlignment="Top"
SelectionChanged="CbxAudioInputs_SelectionChanged" />
DisplayMemberPath="FriendlyName"
ItemsSource="{Binding AvailableRecordDevices, Mode=TwoWay}"
SelectedIndex="0"
SelectedItem="{Binding SelectedDevice, Mode=TwoWay}" />
<Button
x:Name="btnStartRecording"
Width="91"
Height="23"
Margin="0,0,90,49"
Margin="0,0,90,10"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Click="BtnStartRecording_Click"
Command="{Binding StartRecordingCommand}"
Content="Record"
IsEnabled="False" />
IsEnabled="{Binding EnableStartRecord, Mode=OneWay}" />
<Button
x:Name="btnStopRecording"
Width="75"
Height="23"
Margin="0,0,10,49"
Margin="0,0,10,10"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Click="BtnStopRecording_Click"
Command="{Binding StopRecordingCommand}"
Content="Stop"
IsEnabled="False" />
IsEnabled="{Binding EnableStopRecord, Mode=OneWay}" />
</Grid>
</Window>

View File

@ -1,16 +1,5 @@
using CSCore;
using CSCore.CoreAudioAPI;
using CSCore.SoundIn;
using CSCore.Streams;
using DeepSpeechClient.Interfaces;
using Microsoft.Win32;
using System;
using System.Collections.Concurrent;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using CommonServiceLocator;
using DeepSpeech.WPF.ViewModels;
using System.Windows;
namespace DeepSpeechWPF
@ -20,271 +9,9 @@ namespace DeepSpeechWPF
/// </summary>
public partial class MainWindow : Window
{
private readonly IDeepSpeech _sttClient;
private const uint BEAM_WIDTH = 500;
private const float LM_ALPHA = 0.75f;
private const float LM_BETA = 1.85f;
public MainWindow() => InitializeComponent();
#region Streaming
private readonly WasapiCapture _audioCapture;
private MMDeviceCollection _audioCaptureDevices;
private SoundInSource _soundInSource;
private IWaveSource _convertedSource;
/// <summary>
/// Queue that prevents feeding data to the inference engine if it is busy.
/// </summary>
private ConcurrentQueue<short[]> _bufferQueue = new ConcurrentQueue<short[]>();
private int _threadSafeBoolBackValue = 0;
/// <summary>
/// Lock to process items in the queue one at time.
/// </summary>
public bool IsBusy
{
get => (Interlocked.CompareExchange(ref _threadSafeBoolBackValue, 1, 1) == 1);
set
{
if (value) Interlocked.CompareExchange(ref _threadSafeBoolBackValue, 1, 0);
else Interlocked.CompareExchange(ref _threadSafeBoolBackValue, 0, 1);
}
}
#endregion
public MainWindow()
{
InitializeComponent();
_sttClient = new DeepSpeechClient.DeepSpeech();
//if you want to record from the mic change to this
//_audioCapture = new WasapiCapture();
//we capture the windows audio output
_audioCapture = new WasapiLoopbackCapture();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
LoadAvailableCaptureDevices();
Task.Run(()=>
{
try
{
_sttClient.CreateModel("output_graph.pbmm", BEAM_WIDTH);
Dispatcher.Invoke(() => { EnableControls(); });
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
Dispatcher.Invoke(() => { Close(); });
}
});
}
/// <summary>
/// Loads all the available audio capture devices.
/// </summary>
private void LoadAvailableCaptureDevices()
{
DataFlow dataFlow = DataFlow.Render; //Use render to get output devices
// Use capture to get input devices such a microphone
// DataFlow dataFlow = DataFlow.Capture;
_audioCaptureDevices = MMDeviceEnumerator.EnumerateDevices(dataFlow, DeviceState.Active); //we get only enabled devices
foreach (var device in _audioCaptureDevices)
{
cbxAudioInputs.Items.Add(device.FriendlyName);
}
if (_audioCaptureDevices.Count > 0)
{
cbxAudioInputs.SelectedIndex = 0;
}
}
private void EnableControls()
{
btnEnableLM.IsEnabled = true;
btnOpenFile.IsEnabled = true;
btnStartRecording.IsEnabled = true;
}
private async void BtnTranscript_Click(object sender, RoutedEventArgs e)
{
txtResult.Text = string.Empty;
btnTranscript.IsEnabled = false;
lblStatus.Content = "Running inference...";
Stopwatch watch = new Stopwatch();
var waveBuffer = new NAudio.Wave.WaveBuffer(File.ReadAllBytes(txtFileName.Text));
using (var waveInfo = new NAudio.Wave.WaveFileReader(txtFileName.Text))
{
Console.WriteLine("Running inference....");
watch.Start();
await Task.Run(() =>
{
string speechResult = _sttClient.SpeechToText(waveBuffer.ShortBuffer, Convert.ToUInt32(waveBuffer.MaxSize / 2));
watch.Stop();
Dispatcher.Invoke(() =>
{
txtResult.Text = $"Audio duration: {waveInfo.TotalTime.ToString()} {Environment.NewLine}" +
$"Inference took: {watch.Elapsed.ToString()} {Environment.NewLine}" +
$"Recognized text: {speechResult}";
});
});
}
waveBuffer.Clear();
lblStatus.Content = string.Empty;
btnTranscript.IsEnabled = true;
}
private async void BtnEnableLM_Click(object sender, RoutedEventArgs e)
{
lblStatus.Content = "Loading LM.....";
btnEnableLM.IsEnabled = false;
await Task.Run(() =>
{
try
{
_sttClient.EnableDecoderWithLM("lm.binary", "trie", LM_ALPHA, LM_BETA);
Dispatcher.Invoke(() => lblStatus.Content = "LM loaded.");
}
catch (Exception ex)
{
Dispatcher.Invoke(() => btnEnableLM.IsEnabled = true);
MessageBox.Show(ex.Message);
}
});
}
private void BtnOpenFile_Click(object sender, RoutedEventArgs e)
{
OpenFileDialog dialog = new OpenFileDialog
{
Filter = "wav Files |*.wav",
Multiselect = false,
Title = "Please select a wav file."
};
if ((bool)dialog.ShowDialog())
{
txtFileName.Text = dialog.FileName;
btnTranscript.IsEnabled = true;
}
}
protected override void OnClosing(CancelEventArgs e)
{
_sttClient.Dispose();
base.OnClosing(e);
}
private void CbxAudioInputs_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
{
btnStartRecording.IsEnabled = false;
btnStopRecording.IsEnabled = false;
if (_audioCapture.RecordingState == RecordingState.Recording)
{
_audioCapture.Stop();
_soundInSource.Dispose();
_convertedSource.Dispose();
_audioCapture.DataAvailable -= _capture_DataAvailable;
_sttClient.FreeStream(); //this a good example of FreeStream, the user changed the audio input, so we no longer need the current stream
}
if (_audioCaptureDevices!=null)
{
_audioCapture.Device = _audioCaptureDevices[cbxAudioInputs.SelectedIndex];
}
InitializeAudioCapture(_sttClient.GetModelSampleRate());
}
/// <summary>
/// Initializes the recorder and setup the native stream.
/// </summary>
private void InitializeAudioCapture(int desiredSampleRate)
{
_audioCapture.Initialize();
_audioCapture.DataAvailable += _capture_DataAvailable;
_soundInSource = new SoundInSource(_audioCapture) { FillWithZeros = false };
//create a source, that converts the data provided by the
//soundInSource to required by the deepspeech model
_convertedSource = _soundInSource
.ChangeSampleRate(desiredSampleRate) // sample rate
.ToSampleSource()
.ToWaveSource(16); //bits per sample
_convertedSource = _convertedSource.ToMono();
btnStartRecording.IsEnabled = true;
}
private void _capture_DataAvailable(object sender, DataAvailableEventArgs e)
{
//read data from the converedSource
//important: don't use the e.Data here
//the e.Data contains the raw data provided by the
//soundInSource which won't have the deepspeech required audio format
byte[] buffer = new byte[_convertedSource.WaveFormat.BytesPerSecond / 2];
int read;
//int bytesReadIndex = 0;
//keep reading as long as we still get some data
while ((read = _convertedSource.Read(buffer, 0, buffer.Length)) > 0)
{
short[] sdata = new short[(int)Math.Ceiling(Convert.ToDecimal(read / 2))];
Buffer.BlockCopy(buffer, 0, sdata, 0, read);
_bufferQueue.Enqueue(sdata);
Task.Run(() => OnNewData());
}
}
private void BtnStartRecording_Click(object sender, RoutedEventArgs e)
{
_sttClient.CreateStream();
_audioCapture.Start();
btnStartRecording.IsEnabled = false;
btnStopRecording.IsEnabled = true;
}
private async void BtnStopRecording_Click(object sender, RoutedEventArgs e)
{
btnStartRecording.IsEnabled = false;
btnStopRecording.IsEnabled = false;
_audioCapture.Stop();
await Task.Run(async () =>
{
while (!_bufferQueue.IsEmpty && IsBusy) //we wait for all the queued buffers to be processed
{
await Task.Delay(90);
}
string sttResult = _sttClient.FinishStream();
Dispatcher.Invoke(() => txtResult.Text = sttResult);
});
btnStartRecording.IsEnabled = true;
}
/// <summary>
/// Starts processing data from the queue.
/// </summary>
private void OnNewData()
{
while (!IsBusy && !_bufferQueue.IsEmpty)
{
if (_bufferQueue.TryDequeue(out short[] buffer))
{
IsBusy = true;
_sttClient.FeedAudioContent(buffer, Convert.ToUInt32(buffer.Length));
IsBusy = false;
}
}
}
private void Window_Loaded(object sender, RoutedEventArgs e) =>
DataContext = ServiceLocator.Current.GetInstance<MainWindowViewModel>();
}
}

View File

@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace DeepSpeech.WPF.ViewModels
{
/// <summary>
/// Implementation of <see cref="INotifyPropertyChanged"/> to simplify models.
/// </summary>
public abstract class BindableBase : INotifyPropertyChanged
{
/// <summary>
/// Checks if a property already matches a desired value. Sets the property and
/// notifies listeners only when necessary.
/// </summary>
/// <typeparam name="T">Type of the property.</typeparam>
/// <param name="storage">Reference to a property with both getter and setter.</param>
/// <param name="value">Desired value for the property.</param>
/// <param name="propertyName">Name of the property used to notify listeners. This
/// value is optional and can be provided automatically when invoked from compilers that
/// support CallerMemberName.</param>
/// <returns>True if the value was changed, false if the existing value matched the
/// desired value.</returns>
protected bool SetProperty<T>(ref T backingStore, T value,
[CallerMemberName]string propertyName = "",
Action onChanged = null)
{
if (EqualityComparer<T>.Default.Equals(backingStore, value))
return false;
backingStore = value;
onChanged?.Invoke();
OnPropertyChanged(propertyName);
return true;
}
#region INotifyPropertyChanged
/// <summary>
/// Notifies listeners that a property value has changed.
/// </summary>
/// <param name="propertyName">Name of the property used to notify listeners. This
/// value is optional and can be provided automatically when invoked from compilers
/// that support <see cref="CallerMemberNameAttribute"/>.</param>
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
#endregion
}
}

View File

@ -0,0 +1,422 @@
using AsyncAwaitBestPractices.MVVM;
using CSCore;
using CSCore.CoreAudioAPI;
using CSCore.SoundIn;
using CSCore.Streams;
using DeepSpeechClient.Interfaces;
using GalaSoft.MvvmLight.CommandWpf;
using Microsoft.Win32;
using System;
using System.Collections.Concurrent;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace DeepSpeech.WPF.ViewModels
{
/// <summary>
/// View model of the MainWindow View.
/// </summary>
public class MainWindowViewModel : BindableBase
{
#region Constants
private const int SampleRate = 16000;
private const string LMPath = "lm.binary";
private const string TriePath = "trie";
#endregion
private readonly IDeepSpeech _sttClient;
#region Commands
/// <summary>
/// Gets or sets the command that enables the language model.
/// </summary>
public IAsyncCommand EnableLanguageModelCommand { get; private set; }
/// <summary>
/// Gets or sets the command that runs inference using an audio file.
/// </summary>
public IAsyncCommand InferenceFromFileCommand { get; private set; }
/// <summary>
/// Gets or sets the command that opens a dialog to select an audio file.
/// </summary>
public RelayCommand SelectFileCommand { get; private set; }
/// <summary>
/// Gets or sets the command that starts to record.
/// </summary>
public RelayCommand StartRecordingCommand { get; private set; }
/// <summary>
/// Gets or sets the command that stops the recording and gets the result.
/// </summary>
public IAsyncCommand StopRecordingCommand { get; private set; }
#endregion
#region Streaming
/// <summary>
/// Records the audio of the selected device.
/// </summary>
private WasapiCapture _audioCapture;
/// <summary>
/// Converts the device source into a wavesource.
/// </summary>
private SoundInSource _soundInSource;
/// <summary>
/// Target wave source.(16KHz Mono 16bit for DeepSpeech)
/// </summary>
private IWaveSource _convertedSource;
/// <summary>
/// Queue that prevents feeding data to the inference engine if it is busy.
/// </summary>
private ConcurrentQueue<short[]> _bufferQueue = new ConcurrentQueue<short[]>();
private int _threadSafeBoolBackValue = 0;
/// <summary>
/// Lock to process items in the queue one at time.
/// </summary>
public bool StreamingIsBusy
{
get => (Interlocked.CompareExchange(ref _threadSafeBoolBackValue, 1, 1) == 1);
set
{
if (value) Interlocked.CompareExchange(ref _threadSafeBoolBackValue, 1, 0);
else Interlocked.CompareExchange(ref _threadSafeBoolBackValue, 0, 1);
}
}
#endregion
#region ViewProperties
private bool _enableStartRecord;
/// <summary>
/// Gets or sets record status to control the record command.
/// </summary>
public bool EnableStartRecord
{
get => _enableStartRecord;
set => SetProperty(ref _enableStartRecord, value);
}
private bool _stopRecordStopRecord;
/// <summary>
/// Gets or sets record status to control stop command.
/// </summary>
public bool EnableStopRecord
{
get => _stopRecordStopRecord;
set => SetProperty(ref _stopRecordStopRecord, value,
onChanged: ()=> ((AsyncCommand)StopRecordingCommand).RaiseCanExecuteChanged());
}
private MMDevice _selectedDevice;
/// <summary>
/// Gets or sets the selected recording device.
/// </summary>
public MMDevice SelectedDevice
{
get => _selectedDevice;
set => SetProperty(ref _selectedDevice, value,
onChanged: UpdateSelectedDevice);
}
private string _statusMessage;
/// <summary>
/// Gets or sets status message.
/// </summary>
public string StatusMessage
{
get => _statusMessage;
set => SetProperty(ref _statusMessage, value);
}
private bool _languageModelEnabled;
/// <summary>
/// Gets or sets the language model status.
/// </summary>
private bool LanguageModelEnabled
{
get => _languageModelEnabled;
set => SetProperty(ref _languageModelEnabled, value,
onChanged: () => ((AsyncCommand)EnableLanguageModelCommand).RaiseCanExecuteChanged());
}
private bool _isRunningInference;
/// <summary>
/// Gets or sets whenever the model is running inference.
/// </summary>
private bool IsRunningInference
{
get => _isRunningInference;
set => SetProperty(ref _isRunningInference, value,
onChanged: () => ((AsyncCommand)InferenceFromFileCommand).RaiseCanExecuteChanged());
}
private string _transcription;
/// <summary>
/// Gets or sets the current transcription.
/// </summary>
public string Transcription
{
get => _transcription;
set => SetProperty(ref _transcription, value);
}
private string _audioFilePaht;
/// <summary>
/// Gets or sets the selected audio file path.
/// </summary>
public string AudioFilePath
{
get => _audioFilePaht;
set => SetProperty(ref _audioFilePaht, value);
}
private ObservableCollection<MMDevice> _deviceNames;
/// <summary>
/// Gets or sets the available recording devices.
/// </summary>
public ObservableCollection<MMDevice> AvailableRecordDevices
{
get => _deviceNames;
set => SetProperty(ref _deviceNames, value);
}
#endregion
#region Ctors
public MainWindowViewModel(IDeepSpeech sttClient)
{
_sttClient = sttClient;
EnableLanguageModelCommand = new AsyncCommand(()=>EnableLanguageModelAsync(LMPath,TriePath),
_ => !LanguageModelEnabled);
InferenceFromFileCommand = new AsyncCommand(ExecuteInferenceFromFileAsync,
_ => !IsRunningInference);
SelectFileCommand = new RelayCommand(SelectAudioFile);
StartRecordingCommand = new RelayCommand(StartRecording,
canExecute: CanExecuteStartRecording);
StopRecordingCommand = new AsyncCommand(StopRecordingAsync,
_ => EnableStopRecord);
LoadAvailableCaptureDevices();
}
#endregion
/// <summary>
/// Releases the current capture device and initializes the selected one.
/// </summary>
private void UpdateSelectedDevice()
{
ReleaseCapture();
InitializeAudioCapture();
}
/// <summary>
/// Releases the capture device.
/// </summary>
private void ReleaseCapture()
{
if (_audioCapture != null)
{
_audioCapture.DataAvailable -= Capture_DataAvailable;
_audioCapture.Dispose();
}
}
/// <summary>
/// Command usage to know when the recording can start.
/// </summary>
/// <returns>If the device is not null.</returns>
private bool CanExecuteStartRecording() =>
SelectedDevice != null;
/// <summary>
/// Loads all the available audio capture devices.
/// </summary>
private void LoadAvailableCaptureDevices()
{
AvailableRecordDevices = new ObservableCollection<MMDevice>(
MMDeviceEnumerator.EnumerateDevices(DataFlow.All, DeviceState.Active)); //we get only enabled devices
EnableStartRecord = true;
if (AvailableRecordDevices?.Count != 0)
SelectedDevice = AvailableRecordDevices[0];
}
/// <summary>
/// Initializes the capture source.
/// </summary>
private void InitializeAudioCapture()
{
if (SelectedDevice != null)
{
_audioCapture = SelectedDevice.DataFlow == DataFlow.Capture ?
new WasapiCapture() : new WasapiLoopbackCapture();
_audioCapture.Device = SelectedDevice;
_audioCapture.Initialize();
_audioCapture.DataAvailable += Capture_DataAvailable;
_soundInSource = new SoundInSource(_audioCapture) { FillWithZeros = false };
//create a source, that converts the data provided by the
//soundInSource to required format
_convertedSource = _soundInSource
.ChangeSampleRate(SampleRate) // sample rate
.ToSampleSource()
.ToWaveSource(16); //bits per sample
_convertedSource = _convertedSource.ToMono();
}
}
private void Capture_DataAvailable(object sender, DataAvailableEventArgs e)
{
//read data from the converedSource
//important: don't use the e.Data here
//the e.Data contains the raw data provided by the
//soundInSource which won't have the deepspeech required audio format
byte[] buffer = new byte[_convertedSource.WaveFormat.BytesPerSecond / 2];
int read;
//keep reading as long as we still get some data
while ((read = _convertedSource.Read(buffer, 0, buffer.Length)) > 0)
{
short[] sdata = new short[(int)Math.Ceiling(Convert.ToDecimal(read / 2))];
Buffer.BlockCopy(buffer, 0, sdata, 0, read);
_bufferQueue.Enqueue(sdata);
Task.Run(() => OnNewData());
}
}
/// <summary>
/// Starts processing data from the queue.
/// </summary>
private void OnNewData()
{
while (!StreamingIsBusy && !_bufferQueue.IsEmpty)
{
if (_bufferQueue.TryDequeue(out short[] buffer))
{
StreamingIsBusy = true;
_sttClient.FeedAudioContent(buffer, Convert.ToUInt32(buffer.Length));
StreamingIsBusy = false;
}
}
}
/// <summary>
/// Enables the language model.
/// </summary>
/// <param name="lmPath">Language model path.</param>
/// <param name="triePath">Trie path.</param>
/// <returns>A Task to await.</returns>
public async Task EnableLanguageModelAsync(string lmPath, string triePath)
{
try
{
StatusMessage = "Loading language model...";
const float LM_ALPHA = 0.75f;
const float LM_BETA = 1.85f;
await Task.Run(() => _sttClient.EnableDecoderWithLM(LMPath, TriePath, LM_ALPHA, LM_BETA));
LanguageModelEnabled = true;
StatusMessage = "Language model loaded.";
}
catch (Exception ex)
{
StatusMessage = ex.Message;
}
}
/// <summary>
/// Runs inference and sets the transcription of an audio file.
/// </summary>
/// <returns>A Task to await.</returns>
public async Task ExecuteInferenceFromFileAsync()
{
try
{
IsRunningInference = true;
Transcription = string.Empty;
StatusMessage = "Running inference...";
Stopwatch watch = new Stopwatch();
var waveBuffer = new NAudio.Wave.WaveBuffer(File.ReadAllBytes(AudioFilePath));
using (var waveInfo = new NAudio.Wave.WaveFileReader(AudioFilePath))
{
watch.Start();
string speechResult = await Task.Run(() => _sttClient.SpeechToText(
waveBuffer.ShortBuffer,
Convert.ToUInt32(waveBuffer.MaxSize / 2)));
watch.Stop();
Transcription = $"Audio duration: {waveInfo.TotalTime.ToString()} {Environment.NewLine}" +
$"Inference took: {watch.Elapsed.ToString()} {Environment.NewLine}" +
$"Recognized text: {speechResult}";
}
waveBuffer.Clear();
StatusMessage = string.Empty;
}
catch (Exception ex)
{
StatusMessage = ex.Message;
}
finally
{
IsRunningInference = false;
}
}
/// <summary>
/// Stops the recording and sets the transcription of the closed stream.
/// </summary>
/// <returns>A Task to await.</returns>
private async Task StopRecordingAsync()
{
EnableStopRecord = false;
_audioCapture.Stop();
while (!_bufferQueue.IsEmpty && StreamingIsBusy) //we wait for all the queued buffers to be processed
{
await Task.Delay(90);
}
Transcription = _sttClient.FinishStream();
EnableStartRecord = true;
}
/// <summary>
/// Creates a new stream and starts the recording.
/// </summary>
private void StartRecording()
{
_sttClient.CreateStream();
_audioCapture.Start();
EnableStartRecord = false;
EnableStopRecord = true;
}
/// <summary>
/// Opens a dialog to select an audio file.
/// </summary>
private void SelectAudioFile()
{
OpenFileDialog dialog = new OpenFileDialog
{
Filter = "wav Files |*.wav",
Multiselect = false,
Title = "Please select a wav file."
};
if ((bool)dialog.ShowDialog())
{
AudioFilePath = dialog.FileName;
}
}
}
}

View File

@ -1,5 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="AsyncAwaitBestPractices" version="3.1.0" targetFramework="net462" />
<package id="AsyncAwaitBestPractices.MVVM" version="3.1.0" targetFramework="net462" />
<package id="CommonServiceLocator" version="2.0.2" targetFramework="net462" />
<package id="CSCore" version="1.2.1.2" targetFramework="net462" />
<package id="MvvmLightLibs" version="5.4.1.1" targetFramework="net462" />
<package id="NAudio" version="1.9.0" targetFramework="net462" />
</packages>