Skip to content

Commit

Permalink
implemented dotnet core 2.1 bindings (Picovoice#103)
Browse files Browse the repository at this point in the history
C Sharp binding
  • Loading branch information
HeadhunterXamd authored and kenarsa committed Oct 23, 2018
1 parent 4847149 commit 1fc788c
Show file tree
Hide file tree
Showing 11 changed files with 2,643 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
.idea/
*.pyc
/binding/dotnet/PorcupineCS/obj
/binding/dotnet/.vs/PorcupineCS
/binding/dotnet/PorcupineCS/bin
/binding/dotnet/PorcupineTest/bin
/binding/dotnet/PorcupineTest/obj
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ build always-listening voice-enabled applications/platforms. Porcupine is
* [Integration](#integration)
* [C](#c)
* [Python](#python)
* [Csharp](#csharp)
* [Android](#android)
* [iOS](#ios)
* [Javascript](#javascript)
Expand Down Expand Up @@ -222,6 +223,59 @@ collector.
handle.delete()
```

### csharp

[/binding/dotnet/PorcupineCS/Porcupine.cs](/binding/dotnet/PorcupineCS/Porcupine.cs) provides a c# binding for Porcupine library. Below is a
quick demonstration of how to construct an instance of it to detect multiple keywords concurrently.


```csharp
string model_file_path = ... // The file is available at lib/common/porcupine_params.pv
string keyword_file_path = ...
float sensitivity = 0.5;
Porcupine instance;

instance = new Porcupine(model_file_path, keyword_file_path, sensitivity);

if (instance.Status != PicoVoiceStatus.SUCCESS) {
// error handling logic
}
```

Sensitivity is the parameter that enables developers to trade miss rate for false alarm. It is a floating number within
[0, 1]. A higher sensitivity reduces miss rate at cost of increased false alarm rate.

Now the `instance` can be used to monitor incoming audio stream. Porcupine accepts single channel, 16-bit PCM audio.
The sample rate can be retrieved using `instance.SampleRate()`. Finally, Porcupine accepts input audio in consecutive chunks
(aka frames) the length of each frame can be retrieved using `instance.FrameLength()`.

```csharp
Int16[] GetNextAudioFrame()
{
... // some functionality that gets the next frame
}


while (true) {
Int16[] frame = GetNextAudioFrame();
bool result;
PicoVoiceStatus status = instance.Process(pcm, out result);
if (status != PicoVoiceStatus.SUCCESS) {
// error handling logic
}
if (result) {
// detection event logic/callback
}
}
```

Finally, when done we don't need to release the resources ourself, the garbage collector will fix it.
But if you want to do it yourself.

```csharp
instance.Dispose();
```

### Android

There are two possibilities for integrating Porcupine into an Android application.
Expand Down Expand Up @@ -393,6 +447,9 @@ acquired by WebAssembly using `.release` when done

For more information refer to [binding](/binding/js) and [demo](/demo/js).




## Contributing

If you like to contribute to Porcupine, please read through [CONTRIBUTING.md](CONTRIBUTING.md).
Expand Down
31 changes: 31 additions & 0 deletions binding/dotnet/PorcupineCS.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.28010.2041
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PorcupineCS", "PorcupineCS\PorcupineCS.csproj", "{912583D6-EF30-4B7F-A710-F30A4BCF3A4F}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PorcupineTest", "PorcupineTest\PorcupineTest.csproj", "{839DE713-3B20-4516-A101-CCEA8D364D20}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{912583D6-EF30-4B7F-A710-F30A4BCF3A4F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{912583D6-EF30-4B7F-A710-F30A4BCF3A4F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{912583D6-EF30-4B7F-A710-F30A4BCF3A4F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{912583D6-EF30-4B7F-A710-F30A4BCF3A4F}.Release|Any CPU.Build.0 = Release|Any CPU
{839DE713-3B20-4516-A101-CCEA8D364D20}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{839DE713-3B20-4516-A101-CCEA8D364D20}.Debug|Any CPU.Build.0 = Debug|Any CPU
{839DE713-3B20-4516-A101-CCEA8D364D20}.Release|Any CPU.ActiveCfg = Release|Any CPU
{839DE713-3B20-4516-A101-CCEA8D364D20}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B8384AC6-853A-462B-B283-5F8A1F3F47E8}
EndGlobalSection
EndGlobal
182 changes: 182 additions & 0 deletions binding/dotnet/PorcupineCS/Porcupine.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;

namespace PorcupineCS
{

// ReSharper disable UnusedMember.Global
// ReSharper disable IdentifierTypo
// ReSharper disable InconsistentNaming
public enum PicoVoiceStatus
{
SUCCESS = 0,
OUTOFMEMORY = 1,
IOERROR = 2,
INVALIDARGUMENT = 3
}
// ReSharper restore InconsistentNaming
// ReSharper restore IdentifierTypo
// ReSharper restore UnusedMember.Global

public class Porcupine
{
public PicoVoiceStatus Status { get; private set; }
private const string LIBRARY_NAME = "libpv_porcupine";
private IntPtr _libraryPointer;
private static readonly string _extension = $"{GetExtension()}";

#region PINVOKE

[DllImport(LIBRARY_NAME, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
private static extern PicoVoiceStatus pv_porcupine_init(string modelFilepath, string keywordsFilePath, float sensitivity, out IntPtr pointer);

[DllImport(LIBRARY_NAME, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
private static extern PicoVoiceStatus pv_porcupine_multiple_keywords_init(string modelFilepath, int numberOfKeywords, string[] keywordsFilePaths, float[] sensitivities, out IntPtr pointer);

[DllImport(LIBRARY_NAME, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
private static extern int pv_sample_rate();

[DllImport(LIBRARY_NAME, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
private static extern void pv_porcupine_delete(IntPtr pointer);

[DllImport(LIBRARY_NAME, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
private static extern PicoVoiceStatus pv_porcupine_process(IntPtr pointer, short[] voiceData, out bool results);

[DllImport(LIBRARY_NAME, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
private static extern PicoVoiceStatus pv_porcupine_multiple_keywords_process(IntPtr pointer, short[] voiceData, out int keywordIndex);

[DllImport(LIBRARY_NAME, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
private static extern string pv_porcupine_version();

[DllImport(LIBRARY_NAME, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
private static extern int pv_porcupine_frame_length();

#endregion

/// <summary>
/// Loads Porcupine's shared library and creates an instance of wake word detection object.
/// </summary>
/// <param name="libraryPath">Absolute path to Porcupine's shared library.</param>
/// <param name="modelFilePath">Absolute path to file containing model parameters.</param>
/// <param name="keywordFilePath">Absolute path to keyword file containing hyper-parameters. If not present then 'keyword_file_paths' will be used.</param>
/// <param name="sensitivity">Sensitivity parameter. A higher sensitivity value lowers miss rate at the cost of increased false alarm rate.
/// For more information regarding this parameter refer to 'include/pv_porcupine.h'.
/// If not present then 'sensitivities' is used.</param>
/// <param name="keywordFilePaths"> List of absolute paths to keyword files. Intended to be used for multiple keyword scenario.
/// This parameter is used only when 'keyword_file_path' is not set.</param>
/// <param name="sensitivities"> List of sensitivity parameters. Intended to be used for multiple keyword scenario.
/// This parameter is used only when 'sensitivity' is not set.</param>
public Porcupine(string modelFilePath, string keywordFilePath = null,
float? sensitivity = null, IEnumerable<string> keywordFilePaths = null,
IEnumerable<float> sensitivities = null)
{
if(!File.Exists(LIBRARY_NAME + _extension))
throw new Exception($"the {LIBRARY_NAME} cannot be found.\nThis should be in the same folder as this or on a known path.");
if (keywordFilePath == null)
{
if(keywordFilePaths == null)
throw new ArgumentNullException(nameof(keywordFilePaths));

if(sensitivities == null)
throw new ArgumentNullException(nameof(sensitivities));

Status = pv_porcupine_multiple_keywords_init(modelFilePath, keywordFilePaths.Count(), keywordFilePaths.ToArray(), sensitivities.ToArray(), out _libraryPointer);
}
else
{
if (sensitivity == null)
throw new ArgumentNullException(nameof(sensitivity));

Status = pv_porcupine_init(modelFilePath, keywordFilePath, sensitivity.Value, out _libraryPointer);
}
}

/// <summary>
/// Monitors incoming audio stream for a given keyword.
/// </summary>
/// <param name="data">A frame of audio samples. The number of samples per frame can be attained by calling 'pv_porcupine_frame_length()'.
/// The incoming audio needs to have a sample rate equal to 'pv_sample_rate()' and be 16-bit linearly-encoded.
/// Furthermore, porcupine operates on single channel audio.</param>
/// <param name="result">result Flag indicating if the keyword has been observed ending at the current frame.</param>
/// <returns>Status code. Returns 'PV_STATUS_INVALID_ARGUMENT' on failure.</returns>
public PicoVoiceStatus Process(Int16[] data, out bool result)
{
return pv_porcupine_process(_libraryPointer, data, out result);
}

/// <summary>
/// Monitors incoming audio stream for multiple keywords.
/// </summary>
/// <param name="data">A frame of audio samples. For more information about required audio properties refer to documentation of '<seealso cref="Process"/>'.</param>
/// <param name="index">Index of observed keyword at the end of current frame.
/// Indexing is 0-based and based on the ordering of 'keyword_file_paths' passed to 'pv_porcupine_multiple_keywords_init()'.
/// If no keyword is detected it is set to -1.</param>
/// <returns>Status code. Returns 'PV_STATUS_INVALID_ARGUMENT' on failure.</returns>
public PicoVoiceStatus ProcessMultipleKeywords(Int16[] data, out int index)
{
return pv_porcupine_multiple_keywords_process(_libraryPointer, data, out index);
}

/// <summary>
/// Audio sample rate accepted by Picovoice.
/// </summary>
/// <returns></returns>
public int SampleRate()
{
return pv_sample_rate();
}

/// <summary>
/// Getter for length (number of audio samples) per frame.
/// </summary>
/// <returns></returns>
public int FrameLength()
{
return pv_porcupine_frame_length();
}

/// <summary>
/// Getter for the version string of the library
/// </summary>
/// <returns></returns>
public string GetVersion()
{
return pv_porcupine_version();
}

private static string GetExtension()
{
PlatformID platform = Environment.OSVersion.Platform;
if (platform == PlatformID.MacOSX)
{
return ".dylib";
}

if (platform == PlatformID.Unix)
{
return ".so";
}

if (platform == PlatformID.Win32NT)
{
return ".dll";
}

throw new NotImplementedException("this OS has no binding logic implemented yet.");
}

~Porcupine()
{
pv_porcupine_delete(_libraryPointer);
}

public void Dispose()
{
pv_porcupine_delete(_libraryPointer);
_libraryPointer = IntPtr.Zero;
}
}
}
7 changes: 7 additions & 0 deletions binding/dotnet/PorcupineCS/PorcupineCS.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
</PropertyGroup>

</Project>
Loading

0 comments on commit 1fc788c

Please sign in to comment.