packages feed

silero-vad (empty) → 0.1.0.0

raw patch · 21 files changed

+1374/−0 lines, 21 filesdep +WAVEdep +Win32dep +basebinary-added

Dependencies added: WAVE, Win32, base, derive-storable, tasty, tasty-hunit, unix, unliftio, vector

Files

+ CHANGELOG.md view
@@ -0,0 +1,5 @@+## Changelog++### 0.1.0.0++- Initial release.
+ LICENSE view
@@ -0,0 +1,21 @@+MIT License++Copyright (c) 2024 qwbarch++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.
+ README.lhs view
@@ -0,0 +1,70 @@+# silero-vad-hs++[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) [![Hackage](http://img.shields.io/hackage/v/silero-vad.svg)](https://hackage.haskell.org/package/silero-vad)++Voice activity detection powered by [SileroVAD](https://github.com/snakers4/silero-vad).++## Supported architectures++Tested on ``GHC 9.8``, ``GHC 9.2.8``, and ``GHC 8.10.7``.++- [![build-linux-x64](https://github.com/qwbarch/silero-vad-hs/actions/workflows/linux-x64.yml/badge.svg)](https://github.com/qwbarch/silero-vad-hs/actions/workflows/linux-x64.yml)+- [![build-mac-arm64](https://github.com/qwbarch/silero-vad-hs/actions/workflows/mac-arm64.yml/badge.svg)](https://github.com/qwbarch/silero-vad-hs/actions/workflows/mac-arm64.yml)+- [![build-mac-x64](https://github.com/qwbarch/silero-vad-hs/actions/workflows/mac-x64.yml/badge.svg)](https://github.com/qwbarch/silero-vad-hs/actions/workflows/mac-x64.yml)+- [![build-windows-x64](https://github.com/qwbarch/silero-vad-hs/actions/workflows/windows-x64.yml/badge.svg)](https://github.com/qwbarch/silero-vad-hs/actions/workflows/windows-x64.yml)++## Quick start++This is a literate haskell file. You can run this example via the following:+```bash+nix develop --command bash -c '+  export LD_LIBRARY_PATH=lib:$(nix path-info .#stdenv.cc.cc.lib)/lib+  cabal run --flags="build-readme"+'+```++Necessary language extensions and imports for the example:+```haskell+import qualified Data.Vector.Storable as Vector+import Data.Function ((&))+import Data.WAVE (sampleToDouble, WAVE (waveSamples), getWAVEFile)+import Silero (withVad, withModel, detectSegments, detectSpeech, windowLength)+```++For this example, the [WAVE](https://hackage.haskell.org/package/WAVE) library is used for simplicity.  +Unfortunately, its design is flawed and represents audio in a lazy linked list.  +Prefer using [wave](https://hackage.haskell.org/package/wave) for better performance.++```haskell+main :: IO ()+main = do+  wav <- getWAVEFile "lib/jfk.wav"+```+The functions below expects a ``Vector Float``. This converts it to the expected format.+```haskell+  let samples =+        concat (waveSamples wav)+          & Vector.fromList+          & Vector.map (realToFrac . sampleToDouble)+```+Use ``detectSegments`` to detect the start/end times of voice activity segments.+```haskell+  withVad $ \vad -> do+    segments <- detectSegments vad samples+    print segments+```+Alternatively, use ``detectSpeech`` if you want to detect if speech is found in a single window.  +```haskell+  withModel $ \model -> do+    probability <- detectSpeech model $ Vector.take windowLength samples+    putStrLn $ "Probability: " <> show probability+```++> [!NOTE]+> Audio passed to ``detectSegments`` and ``detectSpeech`` functions have the following requirements:+> - Must be 16khz sample rate.+> - Must be mono channel.+> - Must be 16-bit audio.+>+> When using ``detectSpeech``, audio samples must be of size ``windowLength`` (defined as 512).+> If ``length samples /= windowLength``, the probability will always be 0.
+ README.md view
@@ -0,0 +1,70 @@+# silero-vad-hs++[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) [![Hackage](http://img.shields.io/hackage/v/silero-vad.svg)](https://hackage.haskell.org/package/silero-vad)++Voice activity detection powered by [SileroVAD](https://github.com/snakers4/silero-vad).++## Supported architectures++Tested on ``GHC 9.8``, ``GHC 9.2.8``, and ``GHC 8.10.7``.++- [![build-linux-x64](https://github.com/qwbarch/silero-vad-hs/actions/workflows/linux-x64.yml/badge.svg)](https://github.com/qwbarch/silero-vad-hs/actions/workflows/linux-x64.yml)+- [![build-mac-arm64](https://github.com/qwbarch/silero-vad-hs/actions/workflows/mac-arm64.yml/badge.svg)](https://github.com/qwbarch/silero-vad-hs/actions/workflows/mac-arm64.yml)+- [![build-mac-x64](https://github.com/qwbarch/silero-vad-hs/actions/workflows/mac-x64.yml/badge.svg)](https://github.com/qwbarch/silero-vad-hs/actions/workflows/mac-x64.yml)+- [![build-windows-x64](https://github.com/qwbarch/silero-vad-hs/actions/workflows/windows-x64.yml/badge.svg)](https://github.com/qwbarch/silero-vad-hs/actions/workflows/windows-x64.yml)++## Quick start++This is a literate haskell file. You can run this example via the following:+```bash+nix develop --command bash -c '+  export LD_LIBRARY_PATH=lib:$(nix path-info .#stdenv.cc.cc.lib)/lib+  cabal run --flags="build-readme"+'+```++Necessary language extensions and imports for the example:+```haskell+import qualified Data.Vector.Storable as Vector+import Data.Function ((&))+import Data.WAVE (sampleToDouble, WAVE (waveSamples), getWAVEFile)+import Silero (withVad, withModel, detectSegments, detectSpeech, windowLength)+```++For this example, the [WAVE](https://hackage.haskell.org/package/WAVE) library is used for simplicity.  +Unfortunately, its design is flawed and represents audio in a lazy linked list.  +Prefer using [wave](https://hackage.haskell.org/package/wave) for better performance.++```haskell+main :: IO ()+main = do+  wav <- getWAVEFile "lib/jfk.wav"+```+The functions below expects a ``Vector Float``. This converts it to the expected format.+```haskell+  let samples =+        concat (waveSamples wav)+          & Vector.fromList+          & Vector.map (realToFrac . sampleToDouble)+```+Use ``detectSegments`` to detect the start/end times of voice activity segments.+```haskell+  withVad $ \vad -> do+    segments <- detectSegments vad samples+    print segments+```+Alternatively, use ``detectSpeech`` if you want to detect if speech is found in a single window.  +```haskell+  withModel $ \model -> do+    probability <- detectSpeech model $ Vector.take windowLength samples+    putStrLn $ "Probability: " <> show probability+```++> [!NOTE]+> Audio passed to ``detectSegments`` and ``detectSpeech`` functions have the following requirements:+> - Must be 16khz sample rate.+> - Must be mono channel.+> - Must be 16-bit audio.+>+> When using ``detectSpeech``, audio samples must be of size ``windowLength`` (defined as 512).+> If ``length samples /= windowLength``, the probability will always be 0.
+ cbits/silero/detector.c view
@@ -0,0 +1,193 @@+// Credits: SileroVAD's C# example:+// https://github.com/snakers4/silero-vad/blob/cb25c0c0470cf20d7f33805218831b9620dbb6a7/examples/csharp/SileroVadDetector.cs++#include "detector.h"++void reset_segment(struct SpeechSegment *segment) {+  segment->start_index = -1;+  segment->end_index = -1;+  segment->start_time = -1.0f;+  segment->end_time = -1.0f;+}++int math_min(int a, int b) { return (a < b) ? a : b; }+int math_max(int a, int b) { return (a > b) ? a : b; }++int compare_segment(const void *a, const void *b) {+  return ((struct SpeechSegment *)a)->start_index -+         ((struct SpeechSegment *)b)->start_index;+}++float calculate_time(int offset) {+  return floorf(offset / (float)SAMPLE_RATE * 1000.0f) / 1000.0f;+}++void merge_and_calculate_time(vector(struct SpeechSegment) * in_segments,+                              vector(struct SpeechSegment) * out_segments) {+  if (vector_size(*in_segments) == 0)+    return;+  int left = (*in_segments)[0].start_index;+  int right = (*in_segments)[0].end_index;+  if (vector_size(*in_segments) > 1) {+    qsort(*in_segments, vector_size(*in_segments), sizeof(struct SpeechSegment),+          compare_segment);+    for (int i = 0; i < vector_size(*in_segments); i++) {+      struct SpeechSegment *segment = &(*in_segments)[i];+      if (segment->start_index > right) {+        struct SpeechSegment out_segment;+        out_segment.start_index = left;+        out_segment.end_index = right;+        out_segment.start_time = calculate_time(left);+        out_segment.end_time = calculate_time(right);+        vector_add(out_segments, out_segment);+        fflush(stdout);+        left = segment->start_index;+        right = segment->end_index;+      } else {+        right = math_max(right, segment->end_index);+      }+    }+  }+  struct SpeechSegment out_segment;+  out_segment.start_index = left;+  out_segment.end_index = right;+  out_segment.start_time = calculate_time(left);+  out_segment.end_time = calculate_time(right);+  vector_add(out_segments, out_segment);+}++void detect_segments(struct VoiceDetector *vad, const size_t samples_length,+                     const float *samples, size_t *out_segments_length,+                     struct SpeechSegment **out_segments) {++  size_t frames_length = ceil((double)samples_length / (double)WINDOW_LENGTH);+  struct SpeechSegment current_segment;+  reset_segment(&current_segment);+  vector(struct SpeechSegment) segments = vector_create();+  int temp_end = 0;+  int next_start = 0;+  int previous_end = 0;+  bool triggered = false;+  for (int i = 0; i < frames_length; i++) {+    const int offset = i * WINDOW_LENGTH;+    const bool is_last_frame = offset == (frames_length - 1) * WINDOW_LENGTH;+    float *frame = is_last_frame ? calloc(WINDOW_LENGTH, WINDOW_BYTES)+                                 : (float *)(samples + offset);+    if (is_last_frame) {+      memcpy(frame, samples + offset,+             samples_length - (frames_length - 1) * WINDOW_LENGTH);+    }+    float probability = detect_speech(vad->model, frame);++    if (is_last_frame) {+      free(frame);+    }++    if (probability >= vad->start_threshold && temp_end != 0) {+      temp_end = 0;+      if (next_start < previous_end) {+        next_start = i * WINDOW_LENGTH;+      }+    }++    if (probability >= vad->start_threshold && !triggered) {+      triggered = true;+      current_segment.start_index = i * WINDOW_LENGTH;+      continue;+    }++    if (triggered && i * WINDOW_LENGTH - current_segment.start_index >+                         vad->max_speech_samples) {+      if (previous_end != 0) {+        current_segment.end_index = previous_end;+        vector_add(&segments, current_segment);+        reset_segment(&current_segment);+        if (next_start < previous_end) {+          triggered = false;+        } else {+          current_segment.start_index = next_start;+        }+        previous_end = 0;+        next_start = 0;+        temp_end = 0;+      } else {+        current_segment.end_index = i * WINDOW_LENGTH;+        vector_add(&segments, current_segment);+        reset_segment(&current_segment);+        previous_end = 0;+        next_start = 0;+        temp_end = 0;+        triggered = false;+        continue;+      }+    }++    if (probability < vad->end_threshold && triggered) {+      if (temp_end == 0) {+        temp_end = i * WINDOW_LENGTH;+      }+      if (i * WINDOW_LENGTH - temp_end >+          vad->min_silence_samples_at_max_speech) {+        previous_end = temp_end;+      }+      if (i * WINDOW_LENGTH - temp_end < vad->min_silence_samples) {+        continue;+      } else {+        current_segment.end_index = temp_end;+        if (current_segment.end_index - current_segment.start_index >+            vad->min_silence_samples) {+          vector_add(&segments, current_segment);+          reset_segment(&current_segment);+          previous_end = 0;+          next_start = 0;+          temp_end = 0;+          triggered = false;+          continue;+        }+      }+    }+  }++  if (current_segment.start_index >= 0 &&+      samples_length - current_segment.start_index > vad->min_speech_samples) {+    current_segment.end_index = samples_length;+    vector_add(&segments, current_segment);+  }++  for (int i = 0; i < vector_size(segments); i++) {+    struct SpeechSegment *segment = &segments[i];+    if (i == 0) {+      segment->start_index =+          math_max(0, segment->start_index - vad->speech_pad_samples);+    }+    if (i != vector_size(segments) - 1) {+      struct SpeechSegment *next_segment = &segments[i + 1];+      int silence_duration = next_segment->start_index - segment->end_index;+      if (silence_duration < 2 * vad->speech_pad_samples) {+        segment->end_index += silence_duration / 2;+        next_segment->start_index =+            math_max(0, next_segment->start_index - silence_duration / 2);+      } else {+        segment->end_index = math_min(+            samples_length, segment->end_index + vad->speech_pad_samples);+        next_segment->start_index =+            math_max(0, next_segment->start_index - vad->speech_pad_samples);+      }+    } else {+      segment->end_index = math_min(+          samples_length, segment->end_index + vad->speech_pad_samples);+    }+  }++  vector(struct SpeechSegment) merged_segments = vector_create();+  merge_and_calculate_time(&segments, &merged_segments);++  *out_segments_length = vector_size(merged_segments);+  *out_segments = malloc(*out_segments_length * sizeof(struct SpeechSegment));+  memcpy(*out_segments, merged_segments,+         *out_segments_length * sizeof(struct SpeechSegment));+  vector_free(segments);+  vector_free(merged_segments);++  reset_model(vad->model);+}
+ cbits/silero/detector.h view
@@ -0,0 +1,35 @@+#ifndef SILERO_DETECTOR_H+#define SILERO_DETECTOR_H++#include <stdio.h>+#include "model.h"+#include "math.h"+#include "../vec.h"++struct SpeechSegment {+  int start_index;+  int end_index;+  float start_time;+  float end_time;+};++struct VoiceDetector {+  struct SileroModel *model;+  float start_threshold;+  float end_threshold;+  float min_speech_samples;+  float max_speech_samples;+  float speech_pad_samples;+  float min_silence_samples;+  float min_silence_samples_at_max_speech;+};++void detect_segments(+  struct VoiceDetector *vad,+  const size_t samples_length,+  const float *samples,+  size_t *out_segments_length,+  struct SpeechSegment **out_segments+);++#endif
+ cbits/silero/model.c view
@@ -0,0 +1,125 @@+#include "model.h"++// 16khz sample rate can only have a window size of 512 in the v5 model.+#define STATE_LENGTH 2 * 1 * 128+#define STATE_BYTES STATE_LENGTH * sizeof(float)++#define CONTEXT_LENGTH 64+#define CONTEXT_BYTES CONTEXT_LENGTH * sizeof(float)++#define BUFFER_LENGTH WINDOW_LENGTH + CONTEXT_LENGTH+#define BUFFER_BYTES WINDOW_BYTES + CONTEXT_BYTES++// Log level: Error+#define ORT_LOGGING_LEVEL 3++const int64_t sr_shape[] = {1};+const int64_t state_shape[] = {2, 1, 128};++const char *input_names[] = {"input", "state", "sr"};+const char *output_names[] = {"output", "stateN"};++int64_t get_window_length() { return WINDOW_LENGTH; }+int64_t get_sample_rate() { return SAMPLE_RATE; }++struct SileroModel *load_model(OrtApiBase *(*ortGetApiBase)(),+                               const void *model_path) {+  struct SileroModel *model = malloc(sizeof(struct SileroModel));+  model->api = ortGetApiBase()->GetApi(ORT_API_VERSION);+  (void)model->api->CreateEnv(ORT_LOGGING_LEVEL, "silero-vad-hs", &model->env);+  (void)model->api->CreateSessionOptions(&model->session_options);+  (void)model->api->SetIntraOpNumThreads(model->session_options, 1);+  (void)model->api->SetInterOpNumThreads(model->session_options, 1);+  (void)model->api->SetSessionGraphOptimizationLevel(model->session_options,+                                                     ORT_ENABLE_ALL);+  (void)model->api->CreateSession(model->env, model_path,+                                  model->session_options, &model->session);+  (void)model->api->CreateCpuMemoryInfo(OrtArenaAllocator, OrtMemTypeCPU,+                                        &model->memory_info);+  model->state = calloc(STATE_LENGTH, STATE_BYTES);+  model->buffer = calloc(BUFFER_LENGTH, BUFFER_BYTES);+  model->context = calloc(CONTEXT_LENGTH, CONTEXT_BYTES);+  model->input_shape[0] = 1;+  model->is_first_run = true;+  return model;+}++void release_model(struct SileroModel *model) {+  model->api->ReleaseEnv(model->env);+  model->api->ReleaseSessionOptions(model->session_options);+  model->api->ReleaseSession(model->session);+  model->api->ReleaseMemoryInfo(model->memory_info);+  free(model->state);+  free(model->buffer);+  free(model->context);+  free(model);+}++void reset_model(struct SileroModel *model) {+  memset(model->state, 0.0f, STATE_BYTES);+  memset(model->buffer, 0.0f, BUFFER_BYTES);+  memset(model->context, 0.0f, CONTEXT_BYTES);+  model->is_first_run = true;+}++float detect_speech(struct SileroModel *model, const float *samples) {+  // Add context from previous run.+  if (model->is_first_run) {+    model->is_first_run = false;+    model->input_shape[1] = WINDOW_LENGTH;+    memcpy(model->buffer, samples, WINDOW_BYTES);+  } else {+    model->input_shape[1] = BUFFER_LENGTH;+    memcpy(model->buffer, model->context, CONTEXT_BYTES);+    memcpy(model->buffer + CONTEXT_LENGTH, samples, WINDOW_BYTES);+  }+  // Save context for next run.+  memcpy(model->context, samples + WINDOW_LENGTH - CONTEXT_LENGTH,+         CONTEXT_BYTES);++  // Input tensor (containing the pcm data).+  OrtValue *input_tensor = NULL;+  (void)model->api->CreateTensorWithDataAsOrtValue(+      model->memory_info, model->buffer, model->input_shape[1] * sizeof(float),+      model->input_shape, sizeof(model->input_shape) / sizeof(int64_t),+      ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT, &input_tensor);+  // State tensor.+  OrtValue *state_tensor = NULL;+  (void)model->api->CreateTensorWithDataAsOrtValue(+      model->memory_info, model->state, STATE_BYTES, state_shape,+      sizeof(state_shape) / sizeof(int64_t),+      ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT, &state_tensor);++  // Sample-rate tensor (assumes 16khz).+  int64_t sample_rate[] = {SAMPLE_RATE};+  OrtValue *sr_tensor = NULL;+  (void)model->api->CreateTensorWithDataAsOrtValue(+      model->memory_info, sample_rate, sizeof(int64_t), sr_shape,+      sizeof(sr_shape) / sizeof(int64_t), ONNX_TENSOR_ELEMENT_DATA_TYPE_INT64,+      &sr_tensor);++  // Run inference.+  const OrtValue *input_tensors[] = {input_tensor, state_tensor, sr_tensor};+  OrtValue *output_tensors[] = {NULL, NULL};+  (void)model->api->Run(model->session, NULL, input_names, input_tensors,+                        sizeof(input_tensors) / sizeof(OrtValue *),+                        output_names, sizeof(output_names) / sizeof(char *),+                        output_tensors);++  float *probabilities = NULL;+  float *state_n = NULL;++  (void)model->api->GetTensorMutableData(output_tensors[0],+                                         (void **)&probabilities);+  (void)model->api->GetTensorMutableData(output_tensors[1], (void **)&state_n);++  memcpy(model->state, state_n, STATE_BYTES);++  model->api->ReleaseValue(output_tensors[0]);+  model->api->ReleaseValue(output_tensors[1]);+  model->api->ReleaseValue(input_tensor);+  model->api->ReleaseValue(state_tensor);+  model->api->ReleaseValue(sr_tensor);++  return probabilities[0];+}
+ cbits/silero/model.h view
@@ -0,0 +1,33 @@+#ifndef SILERO_MODEL_H+#define SILERO_MODEL_H++#include "../../lib/onnxruntime/onnxruntime_c_api.h"++#define WINDOW_LENGTH 512+#define WINDOW_BYTES WINDOW_LENGTH * sizeof(float)++#define SAMPLE_RATE 16000++int64_t get_window_length();+int64_t get_sample_rate();++struct SileroModel+{+  const struct OrtApi *api;+  OrtEnv *env;+  OrtSessionOptions *session_options;+  OrtSession *session;+  OrtMemoryInfo *memory_info;+  float *state;+  float *context;+  float *buffer;+  int64_t input_shape[2];+  bool is_first_run;+};++struct SileroModel *load_model(OrtApiBase *(*ortGetApiBase)(), const void *model_path);+void release_model(struct SileroModel *model);+void reset_model(struct SileroModel *model);+float detect_speech(struct SileroModel *model, const float *samples);++#endif
+ cbits/vec.c view
@@ -0,0 +1,139 @@+/*+BSD 3-Clause License++Copyright (c) 2024, Mashpoe+All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++1. Redistributions of source code must retain the above copyright notice, this+   list of conditions and the following disclaimer.++2. Redistributions in binary form must reproduce the above copyright notice,+   this list of conditions and the following disclaimer in the documentation+   and/or other materials provided with the distribution.++3. Neither the name of the copyright holder nor the names of its+   contributors may be used to endorse or promote products derived from+   this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.+*/++#include "vec.h"+#include <string.h>++typedef struct {+  vec_size_t size;+  vec_size_t capacity;+  unsigned char data[];+} vector_header;++vector_header *vector_get_header(vector vec) {+  return &((vector_header *)vec)[-1];+}++vector vector_create(void) {+  vector_header *h = (vector_header *)malloc(sizeof(vector_header));+  h->capacity = 0;+  h->size = 0;++  return &h->data;+}++void vector_free(vector vec) { free(vector_get_header(vec)); }++vec_size_t vector_size(vector vec) { return vector_get_header(vec)->size; }++vec_size_t vector_capacity(vector vec) {+  return vector_get_header(vec)->capacity;+}++vector_header *vector_realloc(vector_header *h, vec_type_t type_size) {+  vec_size_t new_capacity = (h->capacity == 0) ? 1 : h->capacity * 2;+  vector_header *new_h = (vector_header *)realloc(+      h, sizeof(vector_header) + new_capacity * type_size);+  new_h->capacity = new_capacity;++  return new_h;+}++bool vector_has_space(vector_header *h) { return h->capacity - h->size > 0; }++void *_vector_add_dst(vector *vec_addr, vec_type_t type_size) {+  vector_header *h = vector_get_header(*vec_addr);++  if (!vector_has_space(h)) {+    h = vector_realloc(h, type_size);+    *vec_addr = h->data;+  }++  return &h->data[type_size * h->size++];+}++void *_vector_insert_dst(vector *vec_addr, vec_type_t type_size,+                         vec_size_t pos) {+  vector_header *h = vector_get_header(*vec_addr);++  vec_size_t new_length = h->size + 1;++  // make sure there is enough room for the new element+  if (!vector_has_space(h)) {+    h = vector_realloc(h, type_size);+    *vec_addr = h->data;+  }+  // move trailing elements+  memmove(&h->data[(pos + 1) * type_size], &h->data[pos * type_size],+          (h->size - pos) * type_size);++  h->size = new_length;++  return &h->data[pos * type_size];+}++void _vector_erase(vector vec, vec_type_t type_size, vec_size_t pos,+                   vec_size_t len) {+  vector_header *h = vector_get_header(vec);+  memmove(&h->data[pos * type_size], &h->data[(pos + len) * type_size],+          (h->size - pos - len) * type_size);++  h->size -= len;+}++void _vector_remove(vector vec, vec_type_t type_size, vec_size_t pos) {+  _vector_erase(vec, type_size, pos, 1);+}++void vector_pop(vector vec) { --vector_get_header(vec)->size; }++void _vector_reserve(vector *vec_addr, vec_type_t type_size,+                     vec_size_t capacity) {+  vector_header *h = vector_get_header(*vec_addr);+  if (h->capacity >= capacity) {+    return;+  }++  h = (vector_header *)realloc(h, sizeof(vector_header) + capacity * type_size);+  h->capacity = capacity;+  *vec_addr = &h->data;+}++vector _vector_copy(vector vec, vec_type_t type_size) {+  vector_header *h = vector_get_header(vec);+  size_t alloc_size = sizeof(vector_header) + h->size * type_size;+  vector_header *copy_h = (vector_header *)malloc(alloc_size);+  memcpy(copy_h, h, alloc_size);+  copy_h->capacity = copy_h->size;++  return &copy_h->data;+}
+ cbits/vec.h view
@@ -0,0 +1,129 @@+/*+BSD 3-Clause License++Copyright (c) 2024, Mashpoe+All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++1. Redistributions of source code must retain the above copyright notice, this+   list of conditions and the following disclaimer.++2. Redistributions in binary form must reproduce the above copyright notice,+   this list of conditions and the following disclaimer in the documentation+   and/or other materials provided with the distribution.++3. Neither the name of the copyright holder nor the names of its+   contributors may be used to endorse or promote products derived from+   this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.+*/++#ifndef vec_h+#define vec_h++#ifdef __cpp_decltype+#include <type_traits>+#define typeof(T) std::remove_reference<std::add_lvalue_reference<decltype(T)>::type>::type+#endif++#ifdef __cplusplus+extern "C" {+#endif++#include <stdbool.h>+#include <stdlib.h>++#define vector(type) type *++// generic type for internal use+typedef void* vector;+// number of elements in a vector+typedef size_t vec_size_t;+// number of bytes for a type+typedef size_t vec_type_t;++// TODO: more rigorous check for typeof support with different compilers+#if _MSC_VER == 0 || __STDC_VERSION__ >= 202311L || defined __cpp_decltype++// shortcut defines++// vec_addr is a vector* (aka type**)+#define vector_add_dst(vec_addr)\+	((typeof(*vec_addr))(\+	    _vector_add_dst((vector*)vec_addr, sizeof(**vec_addr))\+	))+#define vector_insert_dst(vec_addr, pos)\+	((typeof(*vec_addr))(\+	    _vector_insert_dst((vector*)vec_addr, sizeof(**vec_addr), pos)))++#define vector_add(vec_addr, value)\+	(*vector_add_dst(vec_addr) = value)+#define vector_insert(vec_addr, pos, value)\+	(*vector_insert_dst(vec_addr, pos) = value)++#else++#define vector_add_dst(vec_addr, type)\+	((type*)_vector_add_dst((vector*)vec_addr, sizeof(type)))+#define vector_insert_dst(vec_addr, type, pos)\+	((type*)_vector_insert_dst((vector*)vec_addr, sizeof(type), pos))++#define vector_add(vec_addr, type, value)\+	(*vector_add_dst(vec_addr, type) = value)+#define vector_insert(vec_addr, type, pos, value)\+	(*vector_insert_dst(vec_addr, type, pos) = value)++#endif++// vec is a vector (aka type*)+#define vector_erase(vec, pos, len)\+	(_vector_erase((vector)vec, sizeof(*vec), pos, len))+#define vector_remove(vec, pos)\+	(_vector_remove((vector)vec, sizeof(*vec), pos))++#define vector_reserve(vec_addr, capacity)\+	(_vector_reserve((vector*)vec_addr, sizeof(**vec_addr), capacity))++#define vector_copy(vec)\+	(_vector_copy((vector)vec, sizeof(*vec)))++vector vector_create(void);++void vector_free(vector vec);++void* _vector_add_dst(vector* vec_addr, vec_type_t type_size);++void* _vector_insert_dst(vector* vec_addr, vec_type_t type_size, vec_size_t pos);++void _vector_erase(vector vec_addr, vec_type_t type_size, vec_size_t pos, vec_size_t len);++void _vector_remove(vector vec_addr, vec_type_t type_size, vec_size_t pos);++void vector_pop(vector vec);++void _vector_reserve(vector* vec_addr, vec_type_t type_size, vec_size_t capacity);++vector _vector_copy(vector vec, vec_type_t type_size);++vec_size_t vector_size(vector vec);++vec_size_t vector_capacity(vector vec);++// closing bracket for extern "C"+#ifdef __cplusplus+}+#endif++#endif /* vec_h */
+ lib/jfk.wav view

binary file changed (absent → 352078 bytes)

+ lib/onnxruntime/linux-x64/libonnxruntime.so view

file too large to diff

+ lib/onnxruntime/mac-arm64/libonnxruntime.dylib view

file too large to diff

+ lib/onnxruntime/mac-x64/libonnxruntime.dylib view

file too large to diff

+ lib/onnxruntime/windows-x64/onnxruntime.dll view

file too large to diff

+ lib/silero_vad.onnx view

file too large to diff

+ silero-vad.cabal view
@@ -0,0 +1,216 @@+cabal-version: 1.18++-- This file has been generated from package.yaml by hpack version 0.37.0.+--+-- see: https://github.com/sol/hpack++name:           silero-vad+version:        0.1.0.0+synopsis:       Voice activity detection powered by SileroVAD.+description:    A haskell implentation of SileroVAD, a pre-trained enterprise-grade voice activity detector.+category:       Audio, Sound+homepage:       https://github.com/qwbarch/silero-vad-hs+bug-reports:    https://github.com/qwbarch/silero-vad-hs/issues+author:         qwbarch+maintainer:     qwbarch <qwbarch@gmail.com>+license:        MIT+license-file:   LICENSE+build-type:     Simple+tested-with:+    GHC == 9.8, GHC == 9.2.8, GHC == 8.10.7+extra-doc-files:+    README.md+    CHANGELOG.md+data-files:+    lib/onnxruntime/linux-x64/libonnxruntime.so+    lib/onnxruntime/mac-x64/libonnxruntime.dylib+    lib/onnxruntime/mac-arm64/libonnxruntime.dylib+    lib/onnxruntime/windows-x64/onnxruntime.dll+    lib/silero_vad.onnx+    lib/jfk.wav++flag build-readme+  description: Build the literate haskell README example.+  manual: False+  default: False++library+  exposed-modules:+      Silero+      Silero.Detector+      Silero.Model+  other-modules:+      Paths_silero_vad+  hs-source-dirs:+      src+  default-extensions:+      DataKinds+      DeriveAnyClass+      DeriveGeneric+      DerivingStrategies+      DuplicateRecordFields+      EmptyDataDecls+      ExplicitNamespaces+      FlexibleContexts+      GeneralizedNewtypeDeriving+      LambdaCase+      MultiParamTypeClasses+      NegativeLiterals+      NumericUnderscores+      OverloadedLabels+      OverloadedStrings+      PolyKinds+      QuasiQuotes+      RankNTypes+      ScopedTypeVariables+      StrictData+      TemplateHaskell+      TypeApplications+      TypeOperators+  cc-options: -Wno-unused-result+  include-dirs:+      cbits+  c-sources:+      cbits/silero/detector.c+      cbits/silero/detector.h+      cbits/silero/model.c+      cbits/silero/model.h+      cbits/vec.c+      cbits/vec.h+  build-depends:+      base >=4.14.3.0 && <5+    , derive-storable >=0.3.0.0 && <1+    , unliftio >=0.2.20.0 && <1+    , vector >=0.13.0.0 && <1+  default-language: Haskell2010+  if os(linux) || os(darwin)+    build-depends:+        unix ==2.*+  if os(windows)+    build-depends:+        Win32 ==2.*++executable readme+  main-is: README.lhs+  other-modules:+      Silero+      Silero.Detector+      Silero.Model+      Paths_silero_vad+  hs-source-dirs:+      ./+      src+  default-extensions:+      DataKinds+      DeriveAnyClass+      DeriveGeneric+      DerivingStrategies+      DuplicateRecordFields+      EmptyDataDecls+      ExplicitNamespaces+      FlexibleContexts+      GeneralizedNewtypeDeriving+      LambdaCase+      MultiParamTypeClasses+      NegativeLiterals+      NumericUnderscores+      OverloadedLabels+      OverloadedStrings+      PolyKinds+      QuasiQuotes+      RankNTypes+      ScopedTypeVariables+      StrictData+      TemplateHaskell+      TypeApplications+      TypeOperators+  ghc-options: -threaded -rtsopts -with-rtsopts=-N -flate-specialise -fspecialise-aggressively -Wall -Wno-name-shadowing -pgmL markdown-unlit+  cc-options: -Wno-unused-result+  include-dirs:+      cbits+  c-sources:+      cbits/silero/detector.c+      cbits/silero/detector.h+      cbits/silero/model.c+      cbits/silero/model.h+      cbits/vec.c+      cbits/vec.h+  build-tool-depends:+      markdown-unlit:markdown-unlit+  build-depends:+      WAVE ==0.1.*+    , base >=4.14.3.0 && <5+    , derive-storable >=0.3.0.0 && <1+    , unliftio >=0.2.20.0 && <1+    , vector >=0.13.0.0 && <1+  default-language: Haskell2010+  if os(linux) || os(darwin)+    build-depends:+        unix ==2.*+  if os(windows)+    build-depends:+        Win32 ==2.*+  if !flag(build-readme)+    buildable: False++test-suite test+  type: exitcode-stdio-1.0+  main-is: Main.hs+  other-modules:+      Silero+      Silero.Detector+      Silero.Model+      Paths_silero_vad+  hs-source-dirs:+      src+      test+  default-extensions:+      DataKinds+      DeriveAnyClass+      DeriveGeneric+      DerivingStrategies+      DuplicateRecordFields+      EmptyDataDecls+      ExplicitNamespaces+      FlexibleContexts+      GeneralizedNewtypeDeriving+      LambdaCase+      MultiParamTypeClasses+      NegativeLiterals+      NumericUnderscores+      OverloadedLabels+      OverloadedStrings+      PolyKinds+      QuasiQuotes+      RankNTypes+      ScopedTypeVariables+      StrictData+      TemplateHaskell+      TypeApplications+      TypeOperators+  ghc-options: -threaded -rtsopts -with-rtsopts=-N -flate-specialise -fspecialise-aggressively -Wall -Wno-name-shadowing+  cc-options: -Wno-unused-result+  include-dirs:+      cbits+  c-sources:+      cbits/silero/detector.c+      cbits/silero/detector.h+      cbits/silero/model.c+      cbits/silero/model.h+      cbits/vec.c+      cbits/vec.h+  build-depends:+      WAVE ==0.1.*+    , base >=4.14.3.0 && <5+    , derive-storable >=0.3.0.0 && <1+    , tasty >1 && <2+    , tasty-hunit >0.10 && <1+    , unliftio >=0.2.20.0 && <1+    , vector >=0.13.0.0 && <1+  default-language: Haskell2010+  if os(linux) || os(darwin)+    build-depends:+        unix ==2.*+  if os(windows)+    build-depends:+        Win32 ==2.*
+ src/Silero.hs view
@@ -0,0 +1,7 @@+module Silero (+  module Silero.Detector,+  module Silero.Model,+) where++import Silero.Detector+import Silero.Model
+ src/Silero/Detector.hs view
@@ -0,0 +1,99 @@+module Silero.Detector (+  VoiceDetector (..),+  SpeechSegment (..),+  detectSegments,+  defaultVad,+  withVad,+) where++import Control.Applicative (Applicative (liftA2))+import Control.Exception (bracket, finally)+import Control.Monad (join)+import Control.Monad.IO.Class (MonadIO (liftIO))+import Data.Int (Int32)+import Data.Vector.Storable (Storable, Vector)+import qualified Data.Vector.Storable as Vector+import Foreign (Ptr, Storable (..), free, malloc, nullPtr, peekArray, with)+import Foreign.C (CFloat (..))+import Foreign.Storable.Generic (GStorable)+import GHC.Generics (Generic)+import Silero.Model (SileroModel (..), sampleRate, withModel)+import UnliftIO (MonadUnliftIO)++data VoiceDetector = VoiceDetector+  { model :: SileroModel+  , startThreshold :: Float+  , endThreshold :: Float+  , minSpeechSamples :: Float+  , maxSpeechSamples :: Float+  , speechPadSamples :: Float+  , minSilenceSamples :: Float+  , minSilenceSamplesAtMaxSpeech :: Float+  }+  deriving (Generic, GStorable)++-- |+-- Create a **VoiceDetector**.+-- **Warning: SileroModel holds internal state and is NOT thread safe.**+defaultVad :: SileroModel -> VoiceDetector+defaultVad model =+  VoiceDetector+    { model = model+    , startThreshold = 0.5+    , endThreshold = 0.35+    , minSpeechSamples = fromIntegral sampleRate / 1000.0 * 250.0 -- 250ms.+    , maxSpeechSamples = 1.0 / 0.0 -- Infinity.+    , speechPadSamples = fromIntegral sampleRate / 1000.0 * 30.0 -- 30ms.+    , minSilenceSamples = fromIntegral sampleRate / 1000.0 * 100.0 -- 100ms.+    , minSilenceSamplesAtMaxSpeech = fromIntegral sampleRate / 1000.0 * 98.0 -- 98ms+    }++-- |+-- Create a **VoiceDetector**.+-- **Warning: SileroModel holds internal state and is NOT thread safe.**+withVad :: (MonadUnliftIO m) => (VoiceDetector -> m a) -> m a+withVad runVad = withModel (runVad . defaultVad)++data SpeechSegment = SpeechSegment+  { startIndex :: Int32+  , endIndex :: Int32+  , startTime :: CFloat+  , endTime :: CFloat+  }+  deriving (Show, Read, Eq, Ord, Generic, GStorable)++foreign import ccall "detector.h detect_segments"+  c_detect_segments ::+    Ptr VoiceDetector ->+    Int -> -- samplesLength+    Ptr Float -> -- samples+    Ptr Int -> -- outSegmentsLength+    Ptr (Ptr SpeechSegment) -> -- outSegments+    IO ()++-- |+-- Detect the segments where speech starts and ends.+-- This implicitly resets the model after it finishes.+detectSegments :: (MonadIO m) => VoiceDetector -> Vector Float -> m [SpeechSegment]+detectSegments vad samples =+  liftIO . with vad $ \vadPtr ->+    Vector.unsafeWith samples $ \samplesPtr ->+      withPtr @Int $ \segmentsLengthPtr ->+        withPtr @(Ptr SpeechSegment) $ \segmentsPtr -> do+          poke samplesPtr 0+          poke segmentsPtr nullPtr+          flip finally (free =<< peek segmentsPtr) $ do+            c_detect_segments+              vadPtr+              (Vector.length samples)+              samplesPtr+              segmentsLengthPtr+              segmentsPtr+            join $+              liftA2+                peekArray+                (peek segmentsLengthPtr)+                (peek segmentsPtr)+  where+    withPtr :: forall a b. (Storable a) => (Ptr a -> IO b) -> IO b+    withPtr = bracket (malloc @a) free
+ src/Silero/Model.hs view
@@ -0,0 +1,163 @@+{-# LANGUAGE CPP #-}++module Silero.Model (+  SileroModel (..),+  detectSpeech,+  windowLength,+  resetModel,+  sampleRate,+  withModel,+) where++import Data.Int (Int64)+import Data.Vector.Storable (Vector)+import qualified Data.Vector.Storable as Vector+import Foreign.Storable (Storable (..))+import GHC.Generics (Generic)+import GHC.IO (unsafeDupablePerformIO, unsafePerformIO)+import Paths_silero_vad (getDataFileName)+import UnliftIO (MonadIO (liftIO), MonadUnliftIO, bracket)++#if defined (linux_HOST_OS) || defined (darwin_HOST_OS)++import System.Posix (RTLDFlags (RTLD_NOW), dlsym, dlopen )+import Foreign (FunPtr, Ptr, castPtr)+import Foreign.C (CString, withCString)++#else+++import System.Win32 (getProcAddress, loadLibrary)+import Foreign (FunPtr, Ptr, castPtr, castPtrToFunPtr)+import Foreign.C (CWString, withCWString)++#endif++foreign import ccall "model.h get_window_length" c_get_window_length :: IO Int64++foreign import ccall "model.h get_sample_rate" c_get_sample_rate :: IO Int64++foreign import ccall "model.h release_model" c_release_model :: Ptr () -> IO ()++foreign import ccall "model.h reset_model" c_reset_model :: Ptr () -> IO ()++foreign import ccall "model.h detect_speech" c_detect_speech :: Ptr () -> Ptr Float -> IO Float++#if defined (linux_HOST_OS) || defined (darwin_HOST_OS)++foreign import ccall "model.h load_model" c_load_model :: FunPtr () -> CString -> IO (Ptr ())++#else++foreign import ccall "model.h load_model" c_load_model :: FunPtr () -> CWString -> IO (Ptr ())++#endif++windowLength :: Int+windowLength = fromIntegral $ unsafeDupablePerformIO c_get_window_length++sampleRate :: Int+sampleRate = fromIntegral $ unsafeDupablePerformIO c_get_sample_rate++-- |+-- Holds state to be used for voice activity detection.+-- **Warning**: This is **NOT** thread-safe due to this mutating state internally.+newtype SileroModel = SileroModel+  { api :: Ptr ()+  }+  deriving (Generic)++instance Storable SileroModel where+  sizeOf _ = sizeOf (undefined :: Ptr ())+  alignment _ = alignment (undefined :: Ptr ())+  peek ptr = do+    apiPtr <- peek (castPtr ptr)+    return $ SileroModel apiPtr+  poke ptr (SileroModel apiPtr) = poke (castPtr ptr) apiPtr++libraryPath :: FilePath+#if defined(linux_HOST_OS)++libraryPath = "lib/onnxruntime/linux-x64/libonnxruntime.so"++#elif defined(darwin_HOST_OS) && defined(aarch64_HOST_ARCH)++libraryPath = "lib/onnxruntime/mac-arm64/libonnxruntime.dylib"++#elif defined(darwin_HOST_OS) && !defined(aarch64_HOST_ARCH)++libraryPath = "lib/onnxruntime/mac-x64/libonnxruntime.dylib"++#else++libraryPath = "lib/onnxruntime/windows-x64/onnxruntime.dll"++#endif++{-# NOINLINE onnxruntime #-}+onnxruntime :: FunPtr ()+#if defined (linux_HOST_OS) || defined (darwin_HOST_OS)++onnxruntime =+  unsafePerformIO $+    getDataFileName libraryPath+      >>= flip dlopen [RTLD_NOW]+      >>= (liftIO . flip dlsym "OrtGetApiBase")++#else++onnxruntime =+  unsafePerformIO $+    getDataFileName libraryPath+      >>= loadLibrary+      >>= (fmap castPtrToFunPtr . flip getProcAddress "OrtGetApiBase")++#endif++getModelPath :: IO String+getModelPath = getDataFileName "lib/silero_vad.onnx"++#if defined (linux_HOST_OS) || defined (darwin_HOST_OS)++withModelPath :: (CString -> IO a) -> IO a+withModelPath runModelPath = do+  modelPath <- getModelPath+  withCString modelPath $ runModelPath++#else++withModelPath :: (CWString -> IO a) -> IO a+withModelPath runModelPath = do+  modelPath <- getModelPath+  withCWString modelPath runModelPath++#endif++-- | **Warning: SileroModel holds internal state and is NOT thread safe.**+withModel :: (MonadUnliftIO m) => (SileroModel -> m a) -> m a+withModel runModel = do+  bracket+    (SileroModel <$> liftIO (withModelPath $ c_load_model onnxruntime))+    (liftIO . c_release_model . api)+    runModel++-- | **Warning: SileroModel holds internal state and is NOT thread safe.**+resetModel :: (MonadIO m) => SileroModel -> m ()+resetModel = liftIO . c_reset_model . api++-- |+-- Detect if speech is found within the given audio samples.+-- This has the following requirements:+-- - Must be 16khz sample rate.+-- - Must be mono-channel.+-- - Must be 16-bit audio.+-- - Must contain exactly 512 samples.+--+-- | **Warning: SileroModel holds internal state and is NOT thread safe.**+detectSpeech :: (MonadIO m) => SileroModel -> Vector Float -> m Float+detectSpeech (SileroModel api) samples+  | Vector.length samples /= windowLength =+      return 0.0+  | otherwise =+      liftIO . Vector.unsafeWith samples $+        c_detect_speech api
+ test/Main.hs view
@@ -0,0 +1,69 @@+{-# LANGUAGE OverloadedLists #-}++import Control.Applicative (Applicative (liftA2))+import Control.Monad (join)+import Data.Function ((&))+import Data.Functor (void)+import qualified Data.Vector.Storable as Vector+import Data.WAVE (WAVE (..), getWAVEFile, sampleToDouble)+import Paths_silero_vad (getDataFileName)+import Silero.Detector (SpeechSegment (..), detectSegments, withVad)+import Silero.Model (detectSpeech, windowLength, withModel)+import Test.Tasty (defaultMain, testGroup)+import Test.Tasty.HUnit (testCase, (@?=))++main :: IO ()+main = defaultMain $ testGroup "silero-vad" testTree+  where+    loadSamples = do+      wav <- getWAVEFile =<< getDataFileName "lib/jfk.wav"+      pure $+        concat (waveSamples wav)+          & Vector.fromList+          & Vector.map (realToFrac . sampleToDouble)+    testTree =+      [ testCase "detectSegments should provide valid speech segments" . withVad $ \vad -> do+          samples <- loadSamples+          segments <- detectSegments vad samples+          let expected =+                [ SpeechSegment+                    { startIndex = 4640+                    , endIndex = 35296+                    , startTime = 0.29+                    , endTime = 2.206+                    }+                , SpeechSegment+                    { startIndex = 57376+                    , endIndex = 69600+                    , startTime = 3.586+                    , endTime = 4.35+                    }+                , SpeechSegment+                    { startIndex = 86048+                    , endIndex = 122336+                    , startTime = 5.378+                    , endTime = 7.646+                    }+                , SpeechSegment+                    { startIndex = 130592+                    , endIndex = 169952+                    , startTime = 8.161+                    , endTime = 10.622+                    }+                ]+          segments @?= expected+      , testCase "detectSegments should be equal, given the same input after resetting" . withVad $ \vad -> do+          samples <- loadSamples+          segments1 <- detectSegments vad samples+          segments2 <- detectSegments vad samples+          segments1 @?= segments2+      , testCase "detectSegments should not throw on inputs smaller than windowLength" . withVad $ \vad -> do+          void $ detectSegments vad []+          void $ detectSegments vad [1, 2, 3, 4, 5]+      , testCase "detectSpeech should return 0 when not given windowLength samples" . withModel $ \model -> do+          let test actual expected =+                join $ liftA2 (@?=) (detectSpeech model actual) (pure expected)+          test [] 0.0+          test (Vector.replicate (windowLength - 1) 1.0) 0.0+          test (Vector.replicate (windowLength + 1) -1.0) 0.0+      ]