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 +5/−0
- LICENSE +21/−0
- README.lhs +70/−0
- README.md +70/−0
- cbits/silero/detector.c +193/−0
- cbits/silero/detector.h +35/−0
- cbits/silero/model.c +125/−0
- cbits/silero/model.h +33/−0
- cbits/vec.c +139/−0
- cbits/vec.h +129/−0
- lib/jfk.wav binary
- lib/onnxruntime/linux-x64/libonnxruntime.so too large to diff
- lib/onnxruntime/mac-arm64/libonnxruntime.dylib too large to diff
- lib/onnxruntime/mac-x64/libonnxruntime.dylib too large to diff
- lib/onnxruntime/windows-x64/onnxruntime.dll too large to diff
- lib/silero_vad.onnx too large to diff
- silero-vad.cabal +216/−0
- src/Silero.hs +7/−0
- src/Silero/Detector.hs +99/−0
- src/Silero/Model.hs +163/−0
- test/Main.hs +69/−0
+ 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++[](https://opensource.org/licenses/MIT) [](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``.++- [](https://github.com/qwbarch/silero-vad-hs/actions/workflows/linux-x64.yml)+- [](https://github.com/qwbarch/silero-vad-hs/actions/workflows/mac-arm64.yml)+- [](https://github.com/qwbarch/silero-vad-hs/actions/workflows/mac-x64.yml)+- [](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++[](https://opensource.org/licenses/MIT) [](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``.++- [](https://github.com/qwbarch/silero-vad-hs/actions/workflows/linux-x64.yml)+- [](https://github.com/qwbarch/silero-vad-hs/actions/workflows/mac-arm64.yml)+- [](https://github.com/qwbarch/silero-vad-hs/actions/workflows/mac-x64.yml)+- [](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(¤t_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(¤t_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(¤t_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(¤t_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 ©_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+ ]