mirror of
https://github.com/mozilla/DeepSpeech.git
synced 2025-10-26 11:19:39 +00:00
Move WPF example to MVVM
This commit is contained in:
parent
b70de5a9ba
commit
42e533016c
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
Loading…
Reference in New Issue
Block a user