diff --git a/bindings/ruby/.gitignore b/bindings/ruby/.gitignore
new file mode 100644
index 0000000..596263f
--- /dev/null
+++ b/bindings/ruby/.gitignore
@@ -0,0 +1,19 @@
+/.bundle/
+/.yardoc
+/_yardoc/
+/coverage/
+/doc/
+/pkg/
+/html/
+/spec/reports/
+/tmp/
+*.bundle
+*.so
+*.o
+*.a
+mkmf.log
+target/
+Gemfile.lock
+/vendor/bundle/
+ext/ebur128_stream/.rustc_info.json
+ext/ebur128_stream/release/
diff --git a/bindings/ruby/CHANGELOG.md b/bindings/ruby/CHANGELOG.md
new file mode 100644
index 0000000..bb47cf0
--- /dev/null
+++ b/bindings/ruby/CHANGELOG.md
@@ -0,0 +1,5 @@
+## [Unreleased]
+
+## [0.1.0] - 2026-09-04
+
+- Initial release
diff --git a/bindings/ruby/Gemfile b/bindings/ruby/Gemfile
new file mode 100644
index 0000000..f12f6f0
--- /dev/null
+++ b/bindings/ruby/Gemfile
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+source "https://rubygems.org"
+
+# Specify your gem's dependencies in ebur128_stream.gemspec
+gemspec
diff --git a/bindings/ruby/README.md b/bindings/ruby/README.md
new file mode 100644
index 0000000..778b21a
--- /dev/null
+++ b/bindings/ruby/README.md
@@ -0,0 +1,256 @@
+# EBUR128Stream
+
+A Ruby binding for [ebur128-stream][rust-impl], a streaming, zero-allocation EBU R128 loudness measurement in pure Rust.
+
+## Installation
+
+Install the gem and add to the application's Gemfile by executing:
+
+```bash
+bundle add ebur128_stream
+```
+
+If bundler is not being used to manage dependencies, install the gem by executing:
+
+```bash
+gem install ebur128_stream
+```
+
+## Usage
+
+```ruby
+require "ebur128_stream"
+
+include EBUR128Stream
+
+analyzer = Analyzer.new(
+ channels: [:left, :right],
+ sample_rate: 48_000,
+ modes: [:integrated, :true_peak]
+)
+analyzer.push_interleaved samples
+
+report = analyzer.finalize
+report.integrated_lufs # => -8.030723453035735
+report.true_peak_dbtp # => 20.01377283514713
+```
+
+EBUR128Stream provides two main classes:
+
+* [Analyzer](#analyzer) analyzes audio data and reports loudness.
+* [Normalizer](#normalizer) analyzes audio data and applies the calibrated gain.
+
+
+### Analyzer
+
+EBUR128Stream::Analyzer is a streaming EBU R128 loudness analyzer. It reads audio data and reports statistics such as integrated LUFS, dBTP.
+
+It provides a streaming API, which means you can push chunks of audio data incrementally and get the current status (EBUR128Stream::Snapshot) and the final report (EBUR128Stream::Report).
+
+### Initialization
+
+First, initialize EBUR128Stream::Analyzer:
+
+```ruby
+analyzer = EBUR128Stream::Analyzer.new(
+ # Required. Supports :left, :right, :center, :left_surround, :right_surround, :lfe, :other.
+ channels: [:left, :right],
+
+ # Optional. Must be one of 22_050, 32_000, 44_100, 48_000, 88_200, 96_000, 192_000. Defaults to 48_000.
+ sample_rate: 48_000,
+
+ # Optional. Supports :integrated, :momentary, :short_term, :true_peak, :lra, :all. Defaults to [:all].
+ modes: [:all],
+
+ # Optional. Hint at audio length in seconds so that buffer is allocated first.
+ expected_duration: 60
+)
+```
+
+See [Rust documentation](https://docs.rs/ebur128-stream/latest/ebur128_stream/struct.AnalyzerBuilder.html) for details on arguments.
+
+### Pushing samples
+
+Then, push audio samples to EBUR128Stream::Analyzer#push_interleaved or EBUR128Stream::Analyzer#push_planar as you get them:
+
+```ruby
+while samples = get_samples
+ analyzer.push_interleaved(samples)
+ # Or, analyzer.push_planar samples
+end
+```
+
+Interleaved samples looks like this:
+
+```ruby
+[L1, R1, L2, R2, L3, R3, ...]
+```
+
+(*Note* that it's not a 2-D array (`[[L1, R1], [L2, R2], ...]`) but a flat array.)
+
+Planar samples looks like this:
+
+```ruby
+[[L1, L2, L3, ...], [R1, R2, R3, ...]]
+```
+
+In addition to 1-D or 2-D `Array`s, EBUR128Stream::Analyzer#push_interleaved and EBUR128Stream::Analyzer#push_planar accept MemoryView producers such as:
+
+* `Gst::Sample` from [GStreamer][] gem
+* `Torch::Tensor` from [TorchAudio][] or [TorchCodec][] gem (w/ [NDAV::TorchTensor][])
+* `Numo::NArray` from [Numo::NArray][] or [Numo::NArray Alternative][] gem when generating and processing audio data with it (w/ [NDAV::Numo::NArray][])
+
+TorchAudio example here:
+
+```ruby
+samples, sample_rate = TorchAudio.load("path/to/audio")
+analyzer.push_planar(samples)
+```
+
+### Snapshots
+
+At any point during streaming, you can get the current statistics (EBUR128Stream::Snapshot) by calling EBUR128Stream::Analyzer#snapshot:
+
+```ruby
+snapshot = analyzer.snapshot
+
+snapshot.programme_duration_seconds #=> Current duration
+snapshot.momentary_lufs #=> Loudness sliding 400ms window in LUFS
+snapshot.short_term_lufs #=> Loudness sliding 3s window in LUFS
+snapshot.integrated_lufs #=> Loudness so far in LUFS
+snapshot.true_peak_dbtp #=> True peak so far in dBTP
+snapshot.loudness_range_lu #=> Loudness range in LU
+```
+
+The attributes may be `nil` when there are not enough samples or when the corresponding mode was not specified at initialization.
+
+EBUR128Stream::Snapshot implements `#to_h`:
+
+```ruby
+snapshot.to_h #=> {momentary_lufs: ..., short_term_lufs: ..., ...}
+```
+
+Also, `#deconstruct_keys` is implemented:
+
+```ruby
+snapshot.deconstruct_keys(nil) #=> {momentary_lufs: ..., short_term_lufs: ..., ...}
+
+case analyzer.snapshot
+in EBUR128Stream::Snapshot[true_peak_dbtp: 1.0..] => snapshot
+ $stderr.puts "High true peak found at #{snapshot.programme_duration_seconds}s: #{snapshot.true_peak_dbtp} dBTP"
+else
+ # noop
+end
+```
+
+### Finalization
+
+Finally, call EBUR128Stream::Analyzer#finalize to complete the analysis and get the report:
+
+```ruby
+report = analyzer.finalize
+
+report.programme_duration_seconds #=> Total duration in seconds
+report.integrated_lufs
+report.loudness_range_lu
+report.true_peak_dbtp
+report.momentary_max_lufs #=> Maximum momentary loudness in LUFS
+report.short_term_max_lufs #=> Maximum short term loudness in LUFS
+```
+
+EBUR128Stream::Report also implements `#to_h` and `#deconstruct_keys` like EBUR128Stream::Snapshot.
+
+
+### Normalizer
+
+EBUR128Stream::Normalizer analyzes audio data and normalizes it to the target loudness in place.
+
+#### Initialization
+
+Initialize EBUR128Stream::Normalizer with target loudness.
+
+```ruby
+normalizer = EBUR128Stream::Normalizer.new(
+ # Required.
+ sample_rate: 48_000,
+
+ # Required.
+ channels: [:left, :right],
+
+ # Optional. Target loudness in LUFS. Defaults to -23.0.
+ target_lufs: -14.0,
+
+ # Optional. Cap the post-normalisation true peak at this dBTP value.
+ true_peak_ceiling_dbtp: -1.0,
+)
+```
+
+#### Normalization
+
+Pass interleaved samples to EBUR128Stream::Normalizer#normalize_in_place:
+
+```ruby
+report = normalizer.normalize_in_place(interleaved_samples)
+```
+
+*Note* that it modifies the passed `Array` in place as the method name implies.
+
+This returns EBUR128Stream::NormalizeReport:
+
+```ruby
+report.measured_integrated_lufs #=> Measured integrated loudness before normalization.
+report.measured_true_peak_dbtp # Measured true peak before normalization, in dBTP.
+report.target_lufs #=> Target loudness, in LUFS.
+report.true_peak_ceiling_dbtp #=> True peak ceiling in dBTP.
+report.applied_gain_db #=> Gain that was actually applied in dB.
+report.limited_by_true_peak #=> true if the gain was attenuated to honour the true peak ceiling.
+```
+
+EBUR128Stream::NormalizeReport also implements `#to_h` and `deconstruct_keys`.
+
+Additionally, it accepts MemoryView as a `samples` argument. The MemoryView must be writable.
+
+## Examples
+
+Complete examples are available in the sample directory.
+
+* sample/analyze-wavefile.rb - A basic example of analyzing audio from wave file using pure-Ruby [WaveFile][] gem.
+* sample/analyze-microphone.rb - An example that displays microphone loudness in real time.
+* sample/analyze-planar-data.rb - An example of analyzing planar audio data instead of interleaved data.
+* sample/normalize.rb - An example of normalizing audio data to desired loudness and saving it to a file.
+
+## Development
+
+After checking out the repo, run `bundle install` to install dependencies. Then, run `bundle exec rake test` to run the tests. You can also run `bundle exec rake console` for an interactive prompt that will allow you to experiment.
+
+To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
+
+## Contributing
+
+Bug reports and pull requests are welcome on GitHub at https://github.com/vanjamodrinjak21/ebur128_stream. Mention @KitaitiMakoto for issues and pull requests related to the Ruby binding.
+
+## License
+
+Licensed under either of
+
+- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
+- MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
+
+at your option.
+
+## See also
+
+* [Rust crate][rust-impl]
+* [BS.1770][]
+
+[rust-impl]: https://github.com/vanjamodrinjak21/ebur128-stream
+[Analyzer]: rdoc-ref:EBUR128Stream::Analyzer
+[BS.1770]: https://www.itu.int/rec/R-REC-BS.1770
+[GStreamer]: https://github.com/ruby-gnome/ruby-gnome/tree/main/gstreamer
+[TorchAudio]: https://github.com/ankane/torchaudio-ruby
+[TorchCodec]: https://github.com/ankane/torchcodec-ruby
+[Numo::NArray]: https://ruby-numo.github.io/numo-narray/
+[Numo::NArray Alternative]: https://github.com/yoshoku/numo-narray-alt
+[WaveFile]: https://wavefilegem.com/
+[NDAV::TorchTensor]: https://gitlab.com/KitaitiMakoto/ndav-torch-tensor
+[NDAV::Numo::NArray]: https://gitlab.com/KitaitiMakoto/ndav-numo-narray
diff --git a/bindings/ruby/Rakefile b/bindings/ruby/Rakefile
new file mode 100644
index 0000000..57084c4
--- /dev/null
+++ b/bindings/ruby/Rakefile
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require "rubygems/tasks"
+require "rake/testtask"
+require "kar/dsl"
+require "rdoc/task"
+
+GEMSPEC = Gem::Specification.load("ebur128_stream.gemspec")
+
+LICENSE_FILES = FileList["LICENSE-APACHE", "LICENSE-MIT"]
+LICENSE_FILES.each do |license|
+ file license => "../../#{license}" do |t|
+ copy t.source, t.name
+ end
+end
+
+Gem::Tasks.new
+
+cargo "ebur128_stream"
+
+Rake::TestTask.new(test: :cargo)
+RDoc::Task.new
+
+task default: :test
+
+task sync_version: ["lib/ebur128_stream/version.rb", "ext/ebur128_stream/Cargo.toml"] do |t|
+ require_relative t.sources[0]
+ sh "cargo", "set-version", "--manifest-path", t.sources[1], "--offline", EBUR128Stream::VERSION
+end
+
+task build: [:sync_version, :cargo] + LICENSE_FILES
diff --git a/bindings/ruby/ebur128_stream.gemspec b/bindings/ruby/ebur128_stream.gemspec
new file mode 100644
index 0000000..5609e93
--- /dev/null
+++ b/bindings/ruby/ebur128_stream.gemspec
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require_relative "lib/ebur128_stream/version"
+
+Gem::Specification.new do |spec|
+ spec.name = "ebur128_stream"
+ spec.version = EBUR128Stream::VERSION
+ spec.authors = ["Kitaiti Makoto"]
+ spec.email = ["KitaitiMakoto@gmail.com"]
+ spec.licenses = ["Apache-2.0", "MIT"]
+
+ spec.summary = "Streaming, zero-allocation EBU R128 loudness measurement."
+ spec.description = "Ruby binding for Streaming, zero-allocation EBU R128 loudness measurement in pure Rust."
+ spec.homepage = "https://github.com/KitaitiMakoto/ebur128-stream/tree/ruby/bindings/ruby"
+ spec.required_ruby_version = ">= 3.2.0"
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
+ spec.metadata["homepage_uri"] = spec.homepage
+ spec.metadata["source_code_uri"] = "https://github.com/KitaitiMakoto/ebur128-stream/tree/ruby/bindings/ruby"
+ spec.metadata["changelog_uri"] = "https://github.com/KitaitiMakoto/ebur128-stream/tree/ruby/bindings/ruby/CHANGELOG.md"
+
+ # Uncomment the line below to require MFA for gem pushes.
+ # This helps protect your gem from supply chain attacks by ensuring
+ # no one can publish a new version without multi-factor authentication.
+ # See: https://guides.rubygems.org/mfa-requirement-opt-in/
+ # spec.metadata["rubygems_mfa_required"] = "true"
+
+ # Specify which files should be added to the gem when it is released.
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
+ gemspec = File.basename(__FILE__)
+ spec.files = IO.popen(%w[git ls-files -z], chdir: __dir__, err: IO::NULL) do |ls|
+ ls.readlines("\x0", chomp: true)
+ end + ["LICENSE-APACHE", "LICENSE-MIT"]
+ spec.executables = spec.files.grep(%r{\Abin/}) { |f| File.basename(f) }
+ spec.require_paths = ["lib"]
+ spec.extensions = ["ext/ebur128_stream/Cargo.toml"]
+
+ # Uncomment to register a new dependency of your gem
+ # spec.add_dependency "example-gem", "~> 1.0"
+
+ spec.add_development_dependency "irb"
+ spec.add_development_dependency "rake"
+ spec.add_development_dependency "test-unit"
+ spec.add_development_dependency "rubygems-tasks"
+ spec.add_development_dependency "kar"
+ spec.add_development_dependency "numo-narray-alt"
+ spec.add_development_dependency "ndav-numo-narray"
+
+ # For more information and examples about making a new gem, check out our
+ # guide at: https://guides.rubygems.org/make-your-own-gem/
+end
diff --git a/bindings/ruby/ext/ebur128_stream/Cargo.lock b/bindings/ruby/ext/ebur128_stream/Cargo.lock
new file mode 100644
index 0000000..5ec8a47
--- /dev/null
+++ b/bindings/ruby/ext/ebur128_stream/Cargo.lock
@@ -0,0 +1,359 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "bindgen"
+version = "0.72.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
+dependencies = [
+ "bitflags",
+ "cexpr",
+ "clang-sys",
+ "itertools",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "rustc-hash",
+ "shlex",
+ "syn",
+]
+
+[[package]]
+name = "bitflags"
+version = "2.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
+
+[[package]]
+name = "bytemuck"
+version = "1.25.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
+
+[[package]]
+name = "cexpr"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
+dependencies = [
+ "nom",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "clang-sys"
+version = "1.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a"
+dependencies = [
+ "glob",
+ "libc",
+ "libloading",
+]
+
+[[package]]
+name = "ebur128-stream"
+version = "0.1.0"
+dependencies = [
+ "ebur128-stream 0.2.0",
+ "grey-knights",
+ "magnus",
+ "rb-sys",
+ "rb-sys-env",
+ "rb-sys-test-helpers",
+]
+
+[[package]]
+name = "ebur128-stream"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66e026ac32fee8397cdbaafee0030443af608d6c4832e590905b16c9450b7536"
+dependencies = [
+ "bitflags",
+ "libm",
+ "wide",
+]
+
+[[package]]
+name = "either"
+version = "1.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
+
+[[package]]
+name = "glob"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b"
+
+[[package]]
+name = "grey-knights"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ddc9622bcf1fe8243d34e708e00e4b41ca627a385e1d0684df7ddbe79f1c663"
+dependencies = [
+ "magnus",
+ "rb-sys",
+]
+
+[[package]]
+name = "itertools"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "libc"
+version = "0.2.189"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
+
+[[package]]
+name = "libloading"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
+dependencies = [
+ "cfg-if",
+ "windows-link",
+]
+
+[[package]]
+name = "libm"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
+
+[[package]]
+name = "magnus"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b36a5b126bbe97eb0d02d07acfeb327036c6319fd816139a49824a83b7f9012"
+dependencies = [
+ "magnus-macros",
+ "rb-sys",
+ "rb-sys-env",
+ "seq-macro",
+]
+
+[[package]]
+name = "magnus-macros"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47607461fd8e1513cb4f2076c197d8092d921a1ea75bd08af97398f593751892"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "memchr"
+version = "2.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rb-sys"
+version = "0.9.130"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02faf625bb10ba893e3ae620f19c9fb1b5f8fcae0fe4eb86bb3f2230fad75edb"
+dependencies = [
+ "rb-sys-build",
+]
+
+[[package]]
+name = "rb-sys-build"
+version = "0.9.130"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05be6f9c86fe5482808162826f6c047d093b3670582296c770de1d94f8066694"
+dependencies = [
+ "bindgen",
+ "lazy_static",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "shell-words",
+ "syn",
+]
+
+[[package]]
+name = "rb-sys-env"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cca7ad6a7e21e72151d56fe2495a259b5670e204c3adac41ee7ef676ea08117a"
+
+[[package]]
+name = "rb-sys-test-helpers"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccc637c53cad6a6b49ad51c5857c64e36bd70c955ffafe9dc9cc5bc9dd0b3b18"
+dependencies = [
+ "rb-sys",
+ "rb-sys-env",
+ "rb-sys-test-helpers-macros",
+]
+
+[[package]]
+name = "rb-sys-test-helpers-macros"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1c1fce35d9ac77a2745e539232fb82a75856675a5daf4ef8b1ca994d89f8b3f"
+dependencies = [
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "regex"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
+
+[[package]]
+name = "safe_arch"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323"
+dependencies = [
+ "bytemuck",
+]
+
+[[package]]
+name = "seq-macro"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc"
+
+[[package]]
+name = "shell-words"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77"
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "syn"
+version = "2.0.119"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "wide"
+version = "0.7.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03"
+dependencies = [
+ "bytemuck",
+ "safe_arch",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
diff --git a/bindings/ruby/ext/ebur128_stream/Cargo.toml b/bindings/ruby/ext/ebur128_stream/Cargo.toml
new file mode 100644
index 0000000..013a313
--- /dev/null
+++ b/bindings/ruby/ext/ebur128_stream/Cargo.toml
@@ -0,0 +1,21 @@
+[package]
+name = "ebur128-stream"
+version = "0.1.0"
+edition = "2024"
+authors = ["Kitaiti Makoto "]
+publish = false
+
+[lib]
+crate-type = ["cdylib"]
+
+[dependencies]
+magnus = { version = "0.8.2", features = ["rb-sys"] }
+rb-sys = { version = "0.9", features = ["stable-api-compiled-fallback"] }
+ebur128-stream-rs = { package = "ebur128-stream", version = "0.2.0", features= ["normalize", "simd"] }
+grey-knights = { version = "0.1.0" }
+
+[build-dependencies]
+rb-sys-env = "0.2.2"
+
+[dev-dependencies]
+rb-sys-test-helpers = { version = "0.3.0" }
diff --git a/bindings/ruby/ext/ebur128_stream/build.rs b/bindings/ruby/ext/ebur128_stream/build.rs
new file mode 100644
index 0000000..80a7842
--- /dev/null
+++ b/bindings/ruby/ext/ebur128_stream/build.rs
@@ -0,0 +1,5 @@
+pub fn main() -> Result<(), Box> {
+ let _ = rb_sys_env::activate()?;
+
+ Ok(())
+}
diff --git a/bindings/ruby/ext/ebur128_stream/src/analyzer.rs b/bindings/ruby/ext/ebur128_stream/src/analyzer.rs
new file mode 100644
index 0000000..67bac25
--- /dev/null
+++ b/bindings/ruby/ext/ebur128_stream/src/analyzer.rs
@@ -0,0 +1,159 @@
+use crate::{Channels, Error, InterleavedSamples, PlanarSamples, Report, Snapshot};
+use ebur128_stream_rs as engine;
+use magnus::{
+ Integer, Module, RArray, RModule, Ruby, Symbol, TryConvert, Value, function, method,
+ prelude::*,
+ scan_args::{get_kwargs, scan_args},
+};
+use std::{
+ cell::{Ref, RefCell, RefMut},
+ time::Duration,
+};
+
+#[magnus::wrap(class = "EBUR128Stream::Analyzer")]
+struct Analyzer {
+ analyzer: RefCell