diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+## Changelog
+
+### 0.1.0.0
+
+- Initial release.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -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.
diff --git a/README.lhs b/README.lhs
new file mode 100644
--- /dev/null
+++ b/README.lhs
@@ -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.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -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.
diff --git a/cbits/silero/detector.c b/cbits/silero/detector.c
new file mode 100644
--- /dev/null
+++ b/cbits/silero/detector.c
@@ -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);
+}
diff --git a/cbits/silero/detector.h b/cbits/silero/detector.h
new file mode 100644
--- /dev/null
+++ b/cbits/silero/detector.h
@@ -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
diff --git a/cbits/silero/model.c b/cbits/silero/model.c
new file mode 100644
--- /dev/null
+++ b/cbits/silero/model.c
@@ -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];
+}
diff --git a/cbits/silero/model.h b/cbits/silero/model.h
new file mode 100644
--- /dev/null
+++ b/cbits/silero/model.h
@@ -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
diff --git a/cbits/vec.c b/cbits/vec.c
new file mode 100644
--- /dev/null
+++ b/cbits/vec.c
@@ -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;
+}
diff --git a/cbits/vec.h b/cbits/vec.h
new file mode 100644
--- /dev/null
+++ b/cbits/vec.h
@@ -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 */
diff --git a/lib/jfk.wav b/lib/jfk.wav
new file mode 100644
Binary files /dev/null and b/lib/jfk.wav differ
diff --git a/lib/onnxruntime/linux-x64/libonnxruntime.so b/lib/onnxruntime/linux-x64/libonnxruntime.so
new file mode 100644
# file too large to diff: lib/onnxruntime/linux-x64/libonnxruntime.so
diff --git a/lib/onnxruntime/mac-arm64/libonnxruntime.dylib b/lib/onnxruntime/mac-arm64/libonnxruntime.dylib
new file mode 100644
# file too large to diff: lib/onnxruntime/mac-arm64/libonnxruntime.dylib
diff --git a/lib/onnxruntime/mac-x64/libonnxruntime.dylib b/lib/onnxruntime/mac-x64/libonnxruntime.dylib
new file mode 100644
# file too large to diff: lib/onnxruntime/mac-x64/libonnxruntime.dylib
diff --git a/lib/onnxruntime/windows-x64/onnxruntime.dll b/lib/onnxruntime/windows-x64/onnxruntime.dll
new file mode 100644
# file too large to diff: lib/onnxruntime/windows-x64/onnxruntime.dll
diff --git a/lib/silero_vad.onnx b/lib/silero_vad.onnx
new file mode 100644
# file too large to diff: lib/silero_vad.onnx
diff --git a/silero-vad.cabal b/silero-vad.cabal
new file mode 100644
--- /dev/null
+++ b/silero-vad.cabal
@@ -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.*
diff --git a/src/Silero.hs b/src/Silero.hs
new file mode 100644
--- /dev/null
+++ b/src/Silero.hs
@@ -0,0 +1,7 @@
+module Silero (
+  module Silero.Detector,
+  module Silero.Model,
+) where
+
+import Silero.Detector
+import Silero.Model
diff --git a/src/Silero/Detector.hs b/src/Silero/Detector.hs
new file mode 100644
--- /dev/null
+++ b/src/Silero/Detector.hs
@@ -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
diff --git a/src/Silero/Model.hs b/src/Silero/Model.hs
new file mode 100644
--- /dev/null
+++ b/src/Silero/Model.hs
@@ -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
diff --git a/test/Main.hs b/test/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/Main.hs
@@ -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
+      ]
