1
0
mirror of https://github.com/Picovoice/porcupine.git synced 2022-01-28 03:27:53 +03:00
Files
Ian Lavery c6f40c00d6 v2.1 (#645)
2022-01-20 17:59:14 -08:00

471 lines
18 KiB
C#

//
// Copyright 2021 Picovoice Inc.
//
// You may not use this file except in compliance with the license. A copy of the license is located in the "LICENSE"
// file accompanying this source.
//
// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
// an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
// specific language governing permissions and limitations under the License.
//
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using UnityEngine;
#if !UNITY_EDITOR && UNITY_ANDROID
using UnityEngine.Networking;
#endif
namespace Pv.Unity
{
public class Porcupine : IDisposable
{
/// <summary>
/// Status codes returned by Porcupine library
/// </summary>
public enum PorcupineStatus
{
SUCCESS = 0,
OUT_OF_MEMORY = 1,
IO_ERROR = 2,
INVALID_ARGUMENT = 3,
STOP_ITERATION = 4,
KEY_ERROR = 5,
INVALID_STATE = 6,
RUNTIME_ERROR = 7,
ACTIVATION_ERROR = 8,
ACTIVATION_LIMIT_REACHED = 9,
ACTIVATION_THROTTLED = 10,
ACTIVATION_REFUSED = 11
}
/// <summary>
/// Built-in keywords
/// </summary>
public enum BuiltInKeyword
{
ALEXA,
AMERICANO,
BLUEBERRY,
BUMBLEBEE,
COMPUTER,
GRAPEFRUIT,
GRASSHOPPER,
HEY_GOOGLE,
HEY_SIRI,
JARVIS,
OK_GOOGLE,
PICOVOICE,
PORCUPINE,
TERMINATOR
}
#if !UNITY_EDITOR && UNITY_IOS
private const string LIBRARY_PATH = "__Internal";
#else
private const string LIBRARY_PATH = "pv_porcupine";
#endif
private IntPtr _libraryPointer = IntPtr.Zero;
[DllImport(LIBRARY_PATH, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
private static extern PorcupineStatus pv_porcupine_init(string accessKey, string modelPath, int numKeywords, string[] keywordPaths, float[] sensitivities, out IntPtr handle);
[DllImport(LIBRARY_PATH, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
private static extern int pv_sample_rate();
[DllImport(LIBRARY_PATH, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
private static extern void pv_porcupine_delete(IntPtr handle);
[DllImport(LIBRARY_PATH, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
private static extern PorcupineStatus pv_porcupine_process(IntPtr handle, short[] pcm, out int keywordIndex);
[DllImport(LIBRARY_PATH, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
private static extern IntPtr pv_porcupine_version();
[DllImport(LIBRARY_PATH, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
private static extern int pv_porcupine_frame_length();
private static readonly string _platform;
private static readonly Dictionary<BuiltInKeyword, string> _builtInKeywordPaths;
public static readonly string DEFAULT_MODEL_PATH;
static Porcupine()
{
_platform = GetPlatform();
_builtInKeywordPaths = GetBuiltInKeywordPaths(_platform);
DEFAULT_MODEL_PATH = GetDefaultModelPath();
}
/// <summary>
/// Creates an instance of the Porcupine wake word engine from built-in keywords.
/// </summary>
/// <param name="accessKey">AccessKey obtained from Picovoice Console (https://console.picovoice.ai/).</param>
/// <param name="modelPath">Absolute path to the file containing model parameters. If not set it will be set to the default location.</param>
/// <param name="keywords">List of built-in keywords for detection.</param>
/// <param name="sensitivities">
/// Sensitivities for detecting keywords. Each value should be a number within [0, 1]. A higher sensitivity results in fewer
/// misses at the cost of increasing the false alarm rate. If not set, 0.5 will be used.
/// </param>
/// <returns>An instance of Porcupine wake word engine.</returns>
public static Porcupine FromBuiltInKeywords(
string accessKey,
IEnumerable<BuiltInKeyword> keywords,
string modelPath = null,
IEnumerable<float> sensitivities = null)
{
if (keywords == null || keywords.Count() == 0)
{
throw new PorcupineInvalidArgumentException("No built-in keywords were specified.");
}
IEnumerable<string> keywordPaths = keywords
.Where(k => _builtInKeywordPaths.ContainsKey(k))
.Select(k => _builtInKeywordPaths[k]);
return new Porcupine(accessKey, modelPath, keywordPaths, sensitivities);
}
/// <summary>
/// Creates an instance of the Porcupine wake word engine.
/// </summary>
/// <param name="accessKey">AccessKey obtained from Picovoice Console (https://console.picovoice.ai/).</param>
/// <param name="modelPath">Absolute path to file containing model parameters.</param>
/// <param name="keywordPaths">A list of absolute paths to keyword model files.</param>
/// <param name="sensitivities">
/// A list of sensitivity values for each keyword. A higher sensitivity value lowers miss rate at the cost of increased
/// false alarm rate. A sensitivity value should be within [0, 1].
/// </param>
public static Porcupine FromKeywordPaths(
string accessKey,
IEnumerable<string> keywordPaths,
string modelPath = null,
IEnumerable<float> sensitivities = null)
{
return new Porcupine(accessKey, modelPath, keywordPaths, sensitivities);
}
/// <summary>
/// Creates an instance of the Porcupine wake word engine.
/// </summary>
/// <param name="accessKey">AccessKey obtained from Picovoice Console (https://picovoice.ai/console/)</param>
/// <param name="modelPath">Absolute path to file containing model parameters.</param>
/// <param name="keywordPaths">A list of absolute paths to keyword model files.</param>
/// <param name="sensitivities">
/// A list of sensitivity values for each keyword. A higher sensitivity value lowers miss rate at the cost of increased
/// false alarm rate. A sensitivity value should be within [0, 1].
/// </param>
private Porcupine(string accessKey, string modelPath, IEnumerable<string> keywordPaths, IEnumerable<float> sensitivities)
{
if (string.IsNullOrEmpty(accessKey))
{
throw new PorcupineInvalidArgumentException("No AccessKey provided to Porcupine");
}
modelPath = modelPath ?? DEFAULT_MODEL_PATH;
if (!File.Exists(modelPath))
{
#if !UNITY_EDITOR && UNITY_ANDROID
try {
modelPath = ExtractResource(modelPath);
} catch {
throw new PorcupineIOException($"Couldn't find model file at '{modelPath}'");
}
#else
throw new PorcupineIOException($"Couldn't find model file at '{modelPath}'");
#endif
}
if (keywordPaths == null || keywordPaths.Count() == 0)
{
throw new PorcupineInvalidArgumentException("No keyword file paths were provided to Porcupine");
}
#if !UNITY_EDITOR && UNITY_ANDROID
List<String> keywordList = keywordPaths.ToList();
for (int i = 0; i < keywordList.Count(); i++)
{
if (!File.Exists(keywordList[i]))
{
try
{
keywordList[i] = ExtractResource(keywordList[i]);
}
catch
{
throw new PorcupineIOException($"Couldn't find keyword file at '{keywordList[i]}'");
}
}
}
keywordPaths = keywordList;
#else
foreach (string path in keywordPaths)
{
if (!File.Exists(path))
{
throw new PorcupineIOException($"Couldn't find keyword file at '{path}'");
}
}
#endif
if (sensitivities == null)
{
sensitivities = Enumerable.Repeat(0.5f, keywordPaths.Count());
}
else
{
if (sensitivities.Any(s => s < 0 || s > 1))
{
throw new PorcupineInvalidArgumentException("Sensitivities should be within [0, 1].");
}
}
if (sensitivities.Count() != keywordPaths.Count())
{
throw new PorcupineInvalidArgumentException($"Number of keywords ({keywordPaths.Count()}) does not match number of sensitivities ({sensitivities.Count()})");
}
PorcupineStatus status = pv_porcupine_init(
accessKey,
modelPath,
keywordPaths.Count(),
keywordPaths.ToArray(),
sensitivities.ToArray(),
out _libraryPointer);
if (status != PorcupineStatus.SUCCESS)
{
throw PorcupineStatusToException(status, "Porcupine init failed.");
}
Version = Marshal.PtrToStringAnsi(pv_porcupine_version());
SampleRate = pv_sample_rate();
FrameLength = pv_porcupine_frame_length();
}
/// <summary>
/// Process a frame of audio with the wake word engine.
/// </summary>
/// <param name="pcm">
/// A frame of audio samples to be assessed by Porcupine. The required audio format is found by calling `.SampleRate` to get the required
/// sample rate and `.FrameLength` to get the required frame size. Audio must be single-channel and 16-bit linearly-encoded.
/// </param>
/// <returns>
/// Index of the detected keyword, or -1 if no detection occurred
/// </returns>
public int Process(short[] pcm)
{
if (pcm.Length != FrameLength)
{
throw new PorcupineInvalidArgumentException(
$"Input audio frame size ({pcm.Length}) was not the size specified by Porcupine engine ({FrameLength}). " +
$"Use Porcupine.FrameLength to get the correct size.");
}
int keywordIndex;
PorcupineStatus status = pv_porcupine_process(_libraryPointer, pcm, out keywordIndex);
if (status != PorcupineStatus.SUCCESS)
{
throw PorcupineStatusToException(status, "Porcupine process failed.");
}
return keywordIndex;
}
/// <summary>
/// Get the audio sample rate required by Porcupine.
/// </summary>
/// <returns>Required sample rate.</returns>
public int SampleRate { get; private set; }
/// <summary>
/// Gets the required number of audio samples per frame.
/// </summary>
/// <returns>Required frame length.</returns>
public int FrameLength { get; private set; }
/// <summary>
/// Gets the version number of the Porcupine library.
/// </summary>
/// <returns>Version of Pocupine</returns>
public string Version { get; private set; }
/// <summary>
/// Coverts status codes to relavent .NET exceptions
/// </summary>
/// <param name="status">Picovoice library status code.</param>
/// <returns>.NET exception</returns>
private static PorcupineException PorcupineStatusToException(
PorcupineStatus status,
string message = "")
{
switch (status)
{
case PorcupineStatus.OUT_OF_MEMORY:
return new PorcupineMemoryException(message);
case PorcupineStatus.IO_ERROR:
return new PorcupineIOException(message);
case PorcupineStatus.INVALID_ARGUMENT:
return new PorcupineInvalidArgumentException(message);
case PorcupineStatus.STOP_ITERATION:
return new PorcupineStopIterationException(message);
case PorcupineStatus.KEY_ERROR:
return new PorcupineKeyException(message);
case PorcupineStatus.INVALID_STATE:
return new PorcupineInvalidStateException(message);
case PorcupineStatus.RUNTIME_ERROR:
return new PorcupineRuntimeException(message);
case PorcupineStatus.ACTIVATION_ERROR:
return new PorcupineActivationException(message);
case PorcupineStatus.ACTIVATION_LIMIT_REACHED:
return new PorcupineActivationLimitException(message);
case PorcupineStatus.ACTIVATION_THROTTLED:
return new PorcupineActivationThrottledException(message);
case PorcupineStatus.ACTIVATION_REFUSED:
return new PorcupineActivationRefusedException(message);
default:
return new PorcupineException("Unmapped error code returned from Porcupine.");
}
}
/// <summary>
/// Frees memory that was set aside for Porcupine
/// </summary>
public void Dispose()
{
if (_libraryPointer != IntPtr.Zero)
{
pv_porcupine_delete(_libraryPointer);
_libraryPointer = IntPtr.Zero;
// ensures finalizer doesn't trigger if already manually disposed
GC.SuppressFinalize(this);
}
}
~Porcupine()
{
Dispose();
}
private static string GetPlatform()
{
switch (Application.platform)
{
case RuntimePlatform.WindowsEditor:
case RuntimePlatform.WindowsPlayer:
return "windows";
case RuntimePlatform.OSXEditor:
case RuntimePlatform.OSXPlayer:
return "mac";
case RuntimePlatform.LinuxEditor:
case RuntimePlatform.LinuxPlayer:
return "linux";
case RuntimePlatform.IPhonePlayer:
return "ios";
case RuntimePlatform.Android:
return "android";
default:
throw new PorcupineRuntimeException(string.Format("Platform '{0}' not supported by Porcupine Unity binding", Application.platform));
}
}
private static string GetDefaultModelPath()
{
#if !UNITY_EDITOR && UNITY_ANDROID
return ExtractResource(Path.Combine(Application.streamingAssetsPath, "porcupine_params.pv"));
#else
return Path.Combine(Application.streamingAssetsPath, "porcupine_params.pv");
#endif
}
private static Dictionary<BuiltInKeyword, string> GetBuiltInKeywordPaths(string platform)
{
#if !UNITY_EDITOR && UNITY_ANDROID
string keywordFilesDir = Path.Combine(Path.Combine(Application.persistentDataPath, "keyword_files"), platform);
if (!Directory.Exists(keywordFilesDir))
{
Directory.CreateDirectory(keywordFilesDir);
}
string assetDir = Path.Combine(Path.Combine(Application.streamingAssetsPath, "keyword_files"), platform);
foreach (string keyword in Enum.GetNames(typeof(BuiltInKeyword)))
{
ExtractResource(Path.Combine(
assetDir,
string.Format("{0}_{1}.ppn", keyword.Replace("_", " ").ToLower(), platform)));
}
#else
string keywordFilesDir = Path.Combine(Application.streamingAssetsPath, "keyword_files", platform);
#endif
Dictionary<BuiltInKeyword, string> keywordPaths = new Dictionary<BuiltInKeyword, string>();
foreach (string keywordFile in Directory.GetFiles(keywordFilesDir))
{
if (Path.GetFileName(keywordFile).EndsWith(".meta"))
{
continue;
}
string enumName = Path.GetFileName(keywordFile).Split('_')[0].Replace(" ", "_").ToUpper();
if (!Enum.IsDefined(typeof(BuiltInKeyword), enumName))
{
continue;
}
BuiltInKeyword builtin = (BuiltInKeyword)Enum.Parse(typeof(BuiltInKeyword), enumName);
keywordPaths.Add(builtin, Path.Combine(keywordFilesDir, keywordFile));
}
return keywordPaths;
}
#if !UNITY_EDITOR && UNITY_ANDROID
public static string ExtractResource(string filePath)
{
if (!filePath.StartsWith(Application.streamingAssetsPath))
{
throw new PorcupineIOException($"File '{filePath}' not found in streaming assets path.");
}
string dstPath = filePath.Replace(Application.streamingAssetsPath, Application.persistentDataPath);
string dstDir = Path.GetDirectoryName(dstPath);
if (!Directory.Exists(dstDir))
{
Directory.CreateDirectory(dstDir);
}
var loadingRequest = UnityWebRequest.Get(filePath);
loadingRequest.SendWebRequest();
while (!loadingRequest.isDone)
{
if (loadingRequest.isNetworkError || loadingRequest.isHttpError)
{
break;
}
}
if (!(loadingRequest.isNetworkError || loadingRequest.isHttpError))
{
File.WriteAllBytes(dstPath, loadingRequest.downloadHandler.data);
}
return dstPath;
}
#endif
}
}