From 81b877ac6253806da4834974eb59234a0ca0ac66 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Tue, 11 Aug 2026 22:42:02 +0900 Subject: [PATCH 001/122] feat: add Ruby binding boilerplate --- bindings/ruby/.gitignore | 14 +++++++ bindings/ruby/CHANGELOG.md | 5 +++ bindings/ruby/Cargo.toml | 13 ++++++ bindings/ruby/Gemfile | 13 ++++++ bindings/ruby/README.md | 35 ++++++++++++++++ bindings/ruby/Rakefile | 22 ++++++++++ bindings/ruby/bin/console | 11 +++++ bindings/ruby/bin/setup | 8 ++++ bindings/ruby/ebur128_stream.gemspec | 46 +++++++++++++++++++++ bindings/ruby/ext/ebur128_stream/Cargo.toml | 19 +++++++++ bindings/ruby/ext/ebur128_stream/build.rs | 5 +++ bindings/ruby/ext/ebur128_stream/extconf.rb | 6 +++ bindings/ruby/ext/ebur128_stream/src/lib.rs | 23 +++++++++++ bindings/ruby/lib/ebur128_stream.rb | 9 ++++ bindings/ruby/lib/ebur128_stream/version.rb | 5 +++ bindings/ruby/sig/ebur128_stream.rbs | 4 ++ bindings/ruby/test/ebur128_stream_test.rb | 15 +++++++ bindings/ruby/test/test_helper.rb | 6 +++ 18 files changed, 259 insertions(+) create mode 100644 bindings/ruby/.gitignore create mode 100644 bindings/ruby/CHANGELOG.md create mode 100644 bindings/ruby/Cargo.toml create mode 100644 bindings/ruby/Gemfile create mode 100644 bindings/ruby/README.md create mode 100644 bindings/ruby/Rakefile create mode 100755 bindings/ruby/bin/console create mode 100755 bindings/ruby/bin/setup create mode 100644 bindings/ruby/ebur128_stream.gemspec create mode 100644 bindings/ruby/ext/ebur128_stream/Cargo.toml create mode 100644 bindings/ruby/ext/ebur128_stream/build.rs create mode 100644 bindings/ruby/ext/ebur128_stream/extconf.rb create mode 100644 bindings/ruby/ext/ebur128_stream/src/lib.rs create mode 100644 bindings/ruby/lib/ebur128_stream.rb create mode 100644 bindings/ruby/lib/ebur128_stream/version.rb create mode 100644 bindings/ruby/sig/ebur128_stream.rbs create mode 100644 bindings/ruby/test/ebur128_stream_test.rb create mode 100644 bindings/ruby/test/test_helper.rb diff --git a/bindings/ruby/.gitignore b/bindings/ruby/.gitignore new file mode 100644 index 0000000..7c512ea --- /dev/null +++ b/bindings/ruby/.gitignore @@ -0,0 +1,14 @@ +/.bundle/ +/.yardoc +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/spec/reports/ +/tmp/ +*.bundle +*.so +*.o +*.a +mkmf.log +target/ diff --git a/bindings/ruby/CHANGELOG.md b/bindings/ruby/CHANGELOG.md new file mode 100644 index 0000000..e0cfe64 --- /dev/null +++ b/bindings/ruby/CHANGELOG.md @@ -0,0 +1,5 @@ +## [Unreleased] + +## [0.1.0] - 2026-08-11 + +- Initial release diff --git a/bindings/ruby/Cargo.toml b/bindings/ruby/Cargo.toml new file mode 100644 index 0000000..3bab3d8 --- /dev/null +++ b/bindings/ruby/Cargo.toml @@ -0,0 +1,13 @@ +# This Cargo.toml is here to let externals tools (IDEs, etc.) know that this is +# a Rust project. Your extensions dependencies should be added to the Cargo.toml +# in the ext/ directory. + +[workspace] +members = ["./ext/ebur128_stream"] +resolver = "2" + +[profile.release] +# By default, debug symbols are stripped from the final binary which makes it +# harder to debug if something goes wrong. It's recommended to keep debug +# symbols in the release build so that you can debug the final binary if needed. +debug = true diff --git a/bindings/ruby/Gemfile b/bindings/ruby/Gemfile new file mode 100644 index 0000000..7b4e0ac --- /dev/null +++ b/bindings/ruby/Gemfile @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +# Specify your gem's dependencies in ebur128_stream.gemspec +gemspec + +gem "irb" +gem "rake", "~> 13.0" + +gem "rake-compiler" + +gem "test-unit", "~> 3.0" diff --git a/bindings/ruby/README.md b/bindings/ruby/README.md new file mode 100644 index 0000000..fe6d076 --- /dev/null +++ b/bindings/ruby/README.md @@ -0,0 +1,35 @@ +# Ebur128Stream + +TODO: Delete this and the text below, and describe your gem + +Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/ebur128_stream`. To experiment with that code, run `bin/console` for an interactive prompt. + +## Installation + +TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org. + +Install the gem and add to the application's Gemfile by executing: + +```bash +bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG +``` + +If bundler is not being used to manage dependencies, install the gem by executing: + +```bash +gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG +``` + +## Usage + +TODO: Write usage instructions here + +## Development + +After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. + +To install this gem onto your local machine, run `bundle exec rake install`. 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/[USERNAME]/ebur128_stream. diff --git a/bindings/ruby/Rakefile b/bindings/ruby/Rakefile new file mode 100644 index 0000000..8eed607 --- /dev/null +++ b/bindings/ruby/Rakefile @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require "bundler/gem_tasks" +require "rake/testtask" + +Rake::TestTask.new(:test) do |t| + t.libs << "test" + t.libs << "lib" + t.test_files = FileList["test/**/*_test.rb"] +end + +require "rb_sys/extensiontask" + +task build: :compile + +GEMSPEC = Gem::Specification.load("ebur128_stream.gemspec") + +RbSys::ExtensionTask.new("ebur128_stream", GEMSPEC) do |ext| + ext.lib_dir = "lib/ebur128_stream" +end + +task default: %i[compile test] diff --git a/bindings/ruby/bin/console b/bindings/ruby/bin/console new file mode 100755 index 0000000..e53b73f --- /dev/null +++ b/bindings/ruby/bin/console @@ -0,0 +1,11 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "bundler/setup" +require "ebur128_stream" + +# You can add fixtures and/or initialization code here to make experimenting +# with your gem easier. You can also use a different console, if you like. + +require "irb" +IRB.start(__FILE__) diff --git a/bindings/ruby/bin/setup b/bindings/ruby/bin/setup new file mode 100755 index 0000000..dce67d8 --- /dev/null +++ b/bindings/ruby/bin/setup @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' +set -vx + +bundle install + +# Do any other automated setup that you need to do here diff --git a/bindings/ruby/ebur128_stream.gemspec b/bindings/ruby/ebur128_stream.gemspec new file mode 100644 index 0000000..3c556fe --- /dev/null +++ b/bindings/ruby/ebur128_stream.gemspec @@ -0,0 +1,46 @@ +# 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.summary = "TODO: Write a short summary, because RubyGems requires one." + spec.description = "TODO: Write a longer description or delete this line." + spec.homepage = "TODO: Put your gem's website or public repo URL here." + spec.required_ruby_version = ">= 3.2.0" + spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'" + spec.metadata["homepage_uri"] = spec.homepage + spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here." + spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here." + + # 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).reject do |f| + (f == gemspec) || + f.start_with?(*%w[bin/ Gemfile .gitignore test/]) + end + end + spec.bindir = "exe" + spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } + spec.require_paths = ["lib"] + spec.extensions = ["ext/ebur128_stream/extconf.rb"] + + # Uncomment to register a new dependency of your gem + # spec.add_dependency "example-gem", "~> 1.0" + spec.add_dependency "rb_sys", "~> 0.9.128" + + # 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.toml b/bindings/ruby/ext/ebur128_stream/Cargo.toml new file mode 100644 index 0000000..8705bbe --- /dev/null +++ b/bindings/ruby/ext/ebur128_stream/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "ebur128_stream" +version = "0.1.0" +edition = "2021" +authors = ["Kitaiti Makoto "] +publish = false + +[lib] +crate-type = ["cdylib"] + +[dependencies] +magnus = { version = "0.8.2" } +rb-sys = { version = "0.9", features = ["stable-api-compiled-fallback"] } + +[build-dependencies] +rb-sys-env = "0.2.2" + +[dev-dependencies] +rb-sys-test-helpers = { version = "0.2.2" } 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/extconf.rb b/bindings/ruby/ext/ebur128_stream/extconf.rb new file mode 100644 index 0000000..30f44d2 --- /dev/null +++ b/bindings/ruby/ext/ebur128_stream/extconf.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +require "mkmf" +require "rb_sys/mkmf" + +create_rust_makefile("ebur128_stream/ebur128_stream") diff --git a/bindings/ruby/ext/ebur128_stream/src/lib.rs b/bindings/ruby/ext/ebur128_stream/src/lib.rs new file mode 100644 index 0000000..b8f30da --- /dev/null +++ b/bindings/ruby/ext/ebur128_stream/src/lib.rs @@ -0,0 +1,23 @@ +use magnus::{function, prelude::*, Error, Ruby}; + +pub fn hello(subject: String) -> String { + format!("Hello {subject}, from Rust!") +} + +#[magnus::init] +fn init(ruby: &Ruby) -> Result<(), Error> { + let module = ruby.define_module("Ebur128Stream")?; + module.define_singleton_method("hello", function!(hello, 1))?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use rb_sys_test_helpers::ruby_test; + use super::hello; + + #[ruby_test] + fn test_hello() { + assert_eq!("Hello world, from Rust!", hello("world".to_string())); + } +} diff --git a/bindings/ruby/lib/ebur128_stream.rb b/bindings/ruby/lib/ebur128_stream.rb new file mode 100644 index 0000000..9e796ab --- /dev/null +++ b/bindings/ruby/lib/ebur128_stream.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require_relative "ebur128_stream/version" +require "ebur128_stream/ebur128_stream" + +module Ebur128Stream + class Error < StandardError; end + # Your code goes here... +end diff --git a/bindings/ruby/lib/ebur128_stream/version.rb b/bindings/ruby/lib/ebur128_stream/version.rb new file mode 100644 index 0000000..f09c273 --- /dev/null +++ b/bindings/ruby/lib/ebur128_stream/version.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module Ebur128Stream + VERSION = "0.1.0" +end diff --git a/bindings/ruby/sig/ebur128_stream.rbs b/bindings/ruby/sig/ebur128_stream.rbs new file mode 100644 index 0000000..a3b5ca6 --- /dev/null +++ b/bindings/ruby/sig/ebur128_stream.rbs @@ -0,0 +1,4 @@ +module Ebur128Stream + VERSION: String + # See the writing guide of rbs: https://github.com/ruby/rbs#guides +end diff --git a/bindings/ruby/test/ebur128_stream_test.rb b/bindings/ruby/test/ebur128_stream_test.rb new file mode 100644 index 0000000..5cafde9 --- /dev/null +++ b/bindings/ruby/test/ebur128_stream_test.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require "test_helper" + +class Ebur128StreamTest < Test::Unit::TestCase + test "VERSION" do + assert do + ::Ebur128Stream.const_defined?(:VERSION) + end + end + + test "something useful" do + assert_equal("expected", "actual") + end +end diff --git a/bindings/ruby/test/test_helper.rb b/bindings/ruby/test/test_helper.rb new file mode 100644 index 0000000..2e503cc --- /dev/null +++ b/bindings/ruby/test/test_helper.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +$LOAD_PATH.unshift File.expand_path("../lib", __dir__) +require "ebur128_stream" + +require "test-unit" From 2f7f0bd4b83a88920fbd115cfc917917a6f9c86c Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Tue, 11 Aug 2026 22:47:37 +0900 Subject: [PATCH 002/122] feat(ruby): add licenses --- bindings/ruby/README.md | 9 +++++++++ bindings/ruby/ebur128_stream.gemspec | 1 + 2 files changed, 10 insertions(+) diff --git a/bindings/ruby/README.md b/bindings/ruby/README.md index fe6d076..c7fe76c 100644 --- a/bindings/ruby/README.md +++ b/bindings/ruby/README.md @@ -33,3 +33,12 @@ To install this gem onto your local machine, run `bundle exec rake install`. To ## Contributing Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/ebur128_stream. + +## 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. diff --git a/bindings/ruby/ebur128_stream.gemspec b/bindings/ruby/ebur128_stream.gemspec index 3c556fe..20ee5bf 100644 --- a/bindings/ruby/ebur128_stream.gemspec +++ b/bindings/ruby/ebur128_stream.gemspec @@ -7,6 +7,7 @@ Gem::Specification.new do |spec| spec.version = Ebur128Stream::VERSION spec.authors = ["Kitaiti Makoto"] spec.email = ["KitaitiMakoto@gmail.com"] + spec.licenses = ["Apache-2.0", "MIT"] spec.summary = "TODO: Write a short summary, because RubyGems requires one." spec.description = "TODO: Write a longer description or delete this line." From cee062d485382d9d55d6ff686fab4fda765c9942 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Tue, 11 Aug 2026 22:51:17 +0900 Subject: [PATCH 003/122] feat(ruby): change name --- bindings/ruby/README.md | 2 +- bindings/ruby/ebur128_stream.gemspec | 2 +- bindings/ruby/ext/ebur128_stream/src/lib.rs | 2 +- bindings/ruby/lib/ebur128_stream.rb | 2 +- bindings/ruby/lib/ebur128_stream/version.rb | 2 +- bindings/ruby/sig/ebur128_stream.rbs | 2 +- bindings/ruby/test/ebur128_stream_test.rb | 4 ++-- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/bindings/ruby/README.md b/bindings/ruby/README.md index c7fe76c..3e13cc3 100644 --- a/bindings/ruby/README.md +++ b/bindings/ruby/README.md @@ -1,4 +1,4 @@ -# Ebur128Stream +# EBUR128Stream TODO: Delete this and the text below, and describe your gem diff --git a/bindings/ruby/ebur128_stream.gemspec b/bindings/ruby/ebur128_stream.gemspec index 20ee5bf..7a140ca 100644 --- a/bindings/ruby/ebur128_stream.gemspec +++ b/bindings/ruby/ebur128_stream.gemspec @@ -4,7 +4,7 @@ require_relative "lib/ebur128_stream/version" Gem::Specification.new do |spec| spec.name = "ebur128_stream" - spec.version = Ebur128Stream::VERSION + spec.version = EBUR128Stream::VERSION spec.authors = ["Kitaiti Makoto"] spec.email = ["KitaitiMakoto@gmail.com"] spec.licenses = ["Apache-2.0", "MIT"] diff --git a/bindings/ruby/ext/ebur128_stream/src/lib.rs b/bindings/ruby/ext/ebur128_stream/src/lib.rs index b8f30da..756a8a2 100644 --- a/bindings/ruby/ext/ebur128_stream/src/lib.rs +++ b/bindings/ruby/ext/ebur128_stream/src/lib.rs @@ -6,7 +6,7 @@ pub fn hello(subject: String) -> String { #[magnus::init] fn init(ruby: &Ruby) -> Result<(), Error> { - let module = ruby.define_module("Ebur128Stream")?; + let module = ruby.define_module("EBUR128Stream")?; module.define_singleton_method("hello", function!(hello, 1))?; Ok(()) } diff --git a/bindings/ruby/lib/ebur128_stream.rb b/bindings/ruby/lib/ebur128_stream.rb index 9e796ab..b92d6ec 100644 --- a/bindings/ruby/lib/ebur128_stream.rb +++ b/bindings/ruby/lib/ebur128_stream.rb @@ -3,7 +3,7 @@ require_relative "ebur128_stream/version" require "ebur128_stream/ebur128_stream" -module Ebur128Stream +module EBUR128Stream class Error < StandardError; end # Your code goes here... end diff --git a/bindings/ruby/lib/ebur128_stream/version.rb b/bindings/ruby/lib/ebur128_stream/version.rb index f09c273..3cd463b 100644 --- a/bindings/ruby/lib/ebur128_stream/version.rb +++ b/bindings/ruby/lib/ebur128_stream/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -module Ebur128Stream +module EBUR128Stream VERSION = "0.1.0" end diff --git a/bindings/ruby/sig/ebur128_stream.rbs b/bindings/ruby/sig/ebur128_stream.rbs index a3b5ca6..2381e34 100644 --- a/bindings/ruby/sig/ebur128_stream.rbs +++ b/bindings/ruby/sig/ebur128_stream.rbs @@ -1,4 +1,4 @@ -module Ebur128Stream +module EBUR128Stream VERSION: String # See the writing guide of rbs: https://github.com/ruby/rbs#guides end diff --git a/bindings/ruby/test/ebur128_stream_test.rb b/bindings/ruby/test/ebur128_stream_test.rb index 5cafde9..0fab0fc 100644 --- a/bindings/ruby/test/ebur128_stream_test.rb +++ b/bindings/ruby/test/ebur128_stream_test.rb @@ -2,10 +2,10 @@ require "test_helper" -class Ebur128StreamTest < Test::Unit::TestCase +class EBUR128StreamTest < Test::Unit::TestCase test "VERSION" do assert do - ::Ebur128Stream.const_defined?(:VERSION) + ::EBUR128Stream.const_defined?(:VERSION) end end From b2c6d63f5a92f4983325c32dd1470f18ae6d3398 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Tue, 11 Aug 2026 22:56:16 +0900 Subject: [PATCH 004/122] feat(ruby): move development depencies from Gemfile to *.gemspec --- bindings/ruby/Gemfile | 7 ------- bindings/ruby/ebur128_stream.gemspec | 5 +++++ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/bindings/ruby/Gemfile b/bindings/ruby/Gemfile index 7b4e0ac..f12f6f0 100644 --- a/bindings/ruby/Gemfile +++ b/bindings/ruby/Gemfile @@ -4,10 +4,3 @@ source "https://rubygems.org" # Specify your gem's dependencies in ebur128_stream.gemspec gemspec - -gem "irb" -gem "rake", "~> 13.0" - -gem "rake-compiler" - -gem "test-unit", "~> 3.0" diff --git a/bindings/ruby/ebur128_stream.gemspec b/bindings/ruby/ebur128_stream.gemspec index 7a140ca..5d03187 100644 --- a/bindings/ruby/ebur128_stream.gemspec +++ b/bindings/ruby/ebur128_stream.gemspec @@ -42,6 +42,11 @@ Gem::Specification.new do |spec| # spec.add_dependency "example-gem", "~> 1.0" spec.add_dependency "rb_sys", "~> 0.9.128" + spec.add_development_dependency "irb" + spec.add_development_dependency "rake" + spec.add_development_dependency "rake-compiler" + spec.add_development_dependency "test-unit" + # For more information and examples about making a new gem, check out our # guide at: https://guides.rubygems.org/make-your-own-gem/ end From 45f7084bc39b9ba42352f50dc4f1d07f0443de0c Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Tue, 11 Aug 2026 23:06:50 +0900 Subject: [PATCH 005/122] feat(ruby): add Gemfile.lock and vendor/bundle to .gitignore --- bindings/ruby/.gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bindings/ruby/.gitignore b/bindings/ruby/.gitignore index 7c512ea..a492c2d 100644 --- a/bindings/ruby/.gitignore +++ b/bindings/ruby/.gitignore @@ -12,3 +12,5 @@ *.a mkmf.log target/ +Gemfile.lock +/vendor/bundle/ From e5a87a3c7588eee42af48786f45559c0c08408b2 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Tue, 11 Aug 2026 23:07:04 +0900 Subject: [PATCH 006/122] feat(ruby): add Cargo.lock --- bindings/ruby/Cargo.lock | 305 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 305 insertions(+) create mode 100644 bindings/ruby/Cargo.lock diff --git a/bindings/ruby/Cargo.lock b/bindings/ruby/Cargo.lock new file mode 100644 index 0000000..c53c629 --- /dev/null +++ b/bindings/ruby/Cargo.lock @@ -0,0 +1,305 @@ +# 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 = "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 = [ + "magnus", + "rb-sys", + "rb-sys-env", + "rb-sys-test-helpers", +] + +[[package]] +name = "either" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[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 = "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.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45ca28513560e56cfb79a62b1fce363c73af170a182024ce880c77ee9429920a" +dependencies = [ + "rb-sys-build", +] + +[[package]] +name = "rb-sys-build" +version = "0.9.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce04b2c55eff3a21aaa623fcc655d94373238e72cac6b3e1a3641ff31649f99a" +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.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6ccb543252549fc28f5d290322e041cd682bd199a8e1caaa813fb6e63dd221e" +dependencies = [ + "rb-sys", + "rb-sys-env", + "rb-sys-test-helpers-macros", +] + +[[package]] +name = "rb-sys-test-helpers-macros" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1508caed999cb659ab1b3308e7b2985186b3b550ef5492dc18da71b560c55615" +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 = "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 = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" From 6fe087e72ac9ab1e5754942337601e34e5fec4a6 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Tue, 11 Aug 2026 23:08:27 +0900 Subject: [PATCH 007/122] feat(ruby): add rubygems-tasks to development depencencies --- bindings/ruby/ebur128_stream.gemspec | 1 + 1 file changed, 1 insertion(+) diff --git a/bindings/ruby/ebur128_stream.gemspec b/bindings/ruby/ebur128_stream.gemspec index 5d03187..3e6dc16 100644 --- a/bindings/ruby/ebur128_stream.gemspec +++ b/bindings/ruby/ebur128_stream.gemspec @@ -46,6 +46,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency "rake" spec.add_development_dependency "rake-compiler" spec.add_development_dependency "test-unit" + spec.add_development_dependency "rubygems-tasks" # For more information and examples about making a new gem, check out our # guide at: https://guides.rubygems.org/make-your-own-gem/ From 14f733109b14da548cfae5fe98259962115b0197 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Tue, 11 Aug 2026 23:09:40 +0900 Subject: [PATCH 008/122] feat(ruby): use rubygems-tasks instead of bundler/gem_tasks --- bindings/ruby/Rakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/ruby/Rakefile b/bindings/ruby/Rakefile index 8eed607..493dbf5 100644 --- a/bindings/ruby/Rakefile +++ b/bindings/ruby/Rakefile @@ -1,6 +1,6 @@ # frozen_string_literal: true -require "bundler/gem_tasks" +require "rubygems/tasks" require "rake/testtask" Rake::TestTask.new(:test) do |t| From 484a3fd169f69a4e6758ac0b0a6c7e7ed5e0567c Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Tue, 11 Aug 2026 23:09:59 +0900 Subject: [PATCH 009/122] feat(ruby): remove needless scripts --- bindings/ruby/Rakefile | 2 ++ bindings/ruby/bin/console | 11 ----------- bindings/ruby/bin/setup | 8 -------- 3 files changed, 2 insertions(+), 19 deletions(-) delete mode 100755 bindings/ruby/bin/console delete mode 100755 bindings/ruby/bin/setup diff --git a/bindings/ruby/Rakefile b/bindings/ruby/Rakefile index 493dbf5..c8a62bc 100644 --- a/bindings/ruby/Rakefile +++ b/bindings/ruby/Rakefile @@ -3,6 +3,8 @@ require "rubygems/tasks" require "rake/testtask" +Gem::Tasks.new + Rake::TestTask.new(:test) do |t| t.libs << "test" t.libs << "lib" diff --git a/bindings/ruby/bin/console b/bindings/ruby/bin/console deleted file mode 100755 index e53b73f..0000000 --- a/bindings/ruby/bin/console +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -require "bundler/setup" -require "ebur128_stream" - -# You can add fixtures and/or initialization code here to make experimenting -# with your gem easier. You can also use a different console, if you like. - -require "irb" -IRB.start(__FILE__) diff --git a/bindings/ruby/bin/setup b/bindings/ruby/bin/setup deleted file mode 100755 index dce67d8..0000000 --- a/bindings/ruby/bin/setup +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -IFS=$'\n\t' -set -vx - -bundle install - -# Do any other automated setup that you need to do here From 3c30c935a02a735bbb36d9c2a200c346fb48ccc2 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Tue, 11 Aug 2026 23:12:22 +0900 Subject: [PATCH 010/122] feat(ruby): use default settings for test task --- bindings/ruby/Rakefile | 6 +----- bindings/ruby/test/{test_helper.rb => helper.rb} | 0 .../test/{ebur128_stream_test.rb => test_ebur128_stream.rb} | 2 +- 3 files changed, 2 insertions(+), 6 deletions(-) rename bindings/ruby/test/{test_helper.rb => helper.rb} (100%) rename bindings/ruby/test/{ebur128_stream_test.rb => test_ebur128_stream.rb} (90%) diff --git a/bindings/ruby/Rakefile b/bindings/ruby/Rakefile index c8a62bc..8ee6384 100644 --- a/bindings/ruby/Rakefile +++ b/bindings/ruby/Rakefile @@ -5,11 +5,7 @@ require "rake/testtask" Gem::Tasks.new -Rake::TestTask.new(:test) do |t| - t.libs << "test" - t.libs << "lib" - t.test_files = FileList["test/**/*_test.rb"] -end +Rake::TestTask.new require "rb_sys/extensiontask" diff --git a/bindings/ruby/test/test_helper.rb b/bindings/ruby/test/helper.rb similarity index 100% rename from bindings/ruby/test/test_helper.rb rename to bindings/ruby/test/helper.rb diff --git a/bindings/ruby/test/ebur128_stream_test.rb b/bindings/ruby/test/test_ebur128_stream.rb similarity index 90% rename from bindings/ruby/test/ebur128_stream_test.rb rename to bindings/ruby/test/test_ebur128_stream.rb index 0fab0fc..e1e5477 100644 --- a/bindings/ruby/test/ebur128_stream_test.rb +++ b/bindings/ruby/test/test_ebur128_stream.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require "test_helper" +require_relative "helper" class EBUR128StreamTest < Test::Unit::TestCase test "VERSION" do From 780fff8795b2e357d12b2d920891e6b66ebf76d8 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Tue, 11 Aug 2026 23:13:14 +0900 Subject: [PATCH 011/122] feat(ruby): move rb_sys from dependency to development dependency --- bindings/ruby/ebur128_stream.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/ruby/ebur128_stream.gemspec b/bindings/ruby/ebur128_stream.gemspec index 3e6dc16..d6e3dab 100644 --- a/bindings/ruby/ebur128_stream.gemspec +++ b/bindings/ruby/ebur128_stream.gemspec @@ -40,13 +40,13 @@ Gem::Specification.new do |spec| # Uncomment to register a new dependency of your gem # spec.add_dependency "example-gem", "~> 1.0" - spec.add_dependency "rb_sys", "~> 0.9.128" spec.add_development_dependency "irb" spec.add_development_dependency "rake" spec.add_development_dependency "rake-compiler" spec.add_development_dependency "test-unit" spec.add_development_dependency "rubygems-tasks" + spec.add_development_dependency "rb_sys" # For more information and examples about making a new gem, check out our # guide at: https://guides.rubygems.org/make-your-own-gem/ From 4a197fd801eb491ed9abba6163a40525402d9997 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Wed, 12 Aug 2026 03:23:16 +0900 Subject: [PATCH 012/122] feat(ruby): add Kar to development dependencies --- bindings/ruby/ebur128_stream.gemspec | 1 + 1 file changed, 1 insertion(+) diff --git a/bindings/ruby/ebur128_stream.gemspec b/bindings/ruby/ebur128_stream.gemspec index d6e3dab..66c6ffe 100644 --- a/bindings/ruby/ebur128_stream.gemspec +++ b/bindings/ruby/ebur128_stream.gemspec @@ -47,6 +47,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency "test-unit" spec.add_development_dependency "rubygems-tasks" spec.add_development_dependency "rb_sys" + spec.add_development_dependency "kar" # For more information and examples about making a new gem, check out our # guide at: https://guides.rubygems.org/make-your-own-gem/ From 86b959a30118010812877507d0078818b996a58f Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Wed, 12 Aug 2026 03:59:23 +0900 Subject: [PATCH 013/122] feat(ruby): use kar to build Rust instead of rb_sys and rake-compiler --- bindings/ruby/.gitignore | 2 ++ bindings/ruby/Cargo.toml | 13 ------------- bindings/ruby/Rakefile | 15 ++++----------- bindings/ruby/ebur128_stream.gemspec | 2 +- bindings/ruby/{ => ext/ebur128_stream}/Cargo.lock | 0 bindings/ruby/ext/ebur128_stream/extconf.rb | 6 ------ 6 files changed, 7 insertions(+), 31 deletions(-) delete mode 100644 bindings/ruby/Cargo.toml rename bindings/ruby/{ => ext/ebur128_stream}/Cargo.lock (100%) delete mode 100644 bindings/ruby/ext/ebur128_stream/extconf.rb diff --git a/bindings/ruby/.gitignore b/bindings/ruby/.gitignore index a492c2d..022858c 100644 --- a/bindings/ruby/.gitignore +++ b/bindings/ruby/.gitignore @@ -14,3 +14,5 @@ mkmf.log target/ Gemfile.lock /vendor/bundle/ +ext/ebur128_stream/.rustc_info.json +ext/ebur128_stream/release/ diff --git a/bindings/ruby/Cargo.toml b/bindings/ruby/Cargo.toml deleted file mode 100644 index 3bab3d8..0000000 --- a/bindings/ruby/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -# This Cargo.toml is here to let externals tools (IDEs, etc.) know that this is -# a Rust project. Your extensions dependencies should be added to the Cargo.toml -# in the ext/ directory. - -[workspace] -members = ["./ext/ebur128_stream"] -resolver = "2" - -[profile.release] -# By default, debug symbols are stripped from the final binary which makes it -# harder to debug if something goes wrong. It's recommended to keep debug -# symbols in the release build so that you can debug the final binary if needed. -debug = true diff --git a/bindings/ruby/Rakefile b/bindings/ruby/Rakefile index 8ee6384..197165d 100644 --- a/bindings/ruby/Rakefile +++ b/bindings/ruby/Rakefile @@ -2,19 +2,12 @@ require "rubygems/tasks" require "rake/testtask" +require "kar/dsl" Gem::Tasks.new -Rake::TestTask.new +cargo "ebur128_stream" -require "rb_sys/extensiontask" +Rake::TestTask.new(test: :cargo) -task build: :compile - -GEMSPEC = Gem::Specification.load("ebur128_stream.gemspec") - -RbSys::ExtensionTask.new("ebur128_stream", GEMSPEC) do |ext| - ext.lib_dir = "lib/ebur128_stream" -end - -task default: %i[compile test] +task default: :test diff --git a/bindings/ruby/ebur128_stream.gemspec b/bindings/ruby/ebur128_stream.gemspec index 66c6ffe..5b1d1f1 100644 --- a/bindings/ruby/ebur128_stream.gemspec +++ b/bindings/ruby/ebur128_stream.gemspec @@ -36,7 +36,7 @@ Gem::Specification.new do |spec| spec.bindir = "exe" spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] - spec.extensions = ["ext/ebur128_stream/extconf.rb"] + spec.extensions = ["ext/ebur128_stream/Cargo.toml"] # Uncomment to register a new dependency of your gem # spec.add_dependency "example-gem", "~> 1.0" diff --git a/bindings/ruby/Cargo.lock b/bindings/ruby/ext/ebur128_stream/Cargo.lock similarity index 100% rename from bindings/ruby/Cargo.lock rename to bindings/ruby/ext/ebur128_stream/Cargo.lock diff --git a/bindings/ruby/ext/ebur128_stream/extconf.rb b/bindings/ruby/ext/ebur128_stream/extconf.rb deleted file mode 100644 index 30f44d2..0000000 --- a/bindings/ruby/ext/ebur128_stream/extconf.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -require "mkmf" -require "rb_sys/mkmf" - -create_rust_makefile("ebur128_stream/ebur128_stream") From 540fb849f410cb6a99fa340fab8f10366d316c64 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Wed, 12 Aug 2026 04:00:10 +0900 Subject: [PATCH 014/122] feat(ruby): remove rb_sys and rake-compiler from dependencies --- bindings/ruby/ebur128_stream.gemspec | 2 -- 1 file changed, 2 deletions(-) diff --git a/bindings/ruby/ebur128_stream.gemspec b/bindings/ruby/ebur128_stream.gemspec index 5b1d1f1..d361da5 100644 --- a/bindings/ruby/ebur128_stream.gemspec +++ b/bindings/ruby/ebur128_stream.gemspec @@ -43,10 +43,8 @@ Gem::Specification.new do |spec| spec.add_development_dependency "irb" spec.add_development_dependency "rake" - spec.add_development_dependency "rake-compiler" spec.add_development_dependency "test-unit" spec.add_development_dependency "rubygems-tasks" - spec.add_development_dependency "rb_sys" spec.add_development_dependency "kar" # For more information and examples about making a new gem, check out our From 9c95f06c95eff0e8bcca268bcc6c017c47db8ac5 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Wed, 12 Aug 2026 04:04:58 +0900 Subject: [PATCH 015/122] feat(ruby): add ebur128-stream create to dependencies --- bindings/ruby/ext/ebur128_stream/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/bindings/ruby/ext/ebur128_stream/Cargo.toml b/bindings/ruby/ext/ebur128_stream/Cargo.toml index 8705bbe..f4cc019 100644 --- a/bindings/ruby/ext/ebur128_stream/Cargo.toml +++ b/bindings/ruby/ext/ebur128_stream/Cargo.toml @@ -11,6 +11,7 @@ crate-type = ["cdylib"] [dependencies] magnus = { version = "0.8.2" } rb-sys = { version = "0.9", features = ["stable-api-compiled-fallback"] } +ebur128-stream = { version = "0.2.0" } [build-dependencies] rb-sys-env = "0.2.2" From 17376b1e1315f58fed1dd5b66bce652e2538e1d9 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Wed, 12 Aug 2026 04:05:19 +0900 Subject: [PATCH 016/122] feat(ruby): install ebur128-stream --- bindings/ruby/ext/ebur128_stream/Cargo.lock | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/bindings/ruby/ext/ebur128_stream/Cargo.lock b/bindings/ruby/ext/ebur128_stream/Cargo.lock index c53c629..f3c0962 100644 --- a/bindings/ruby/ext/ebur128_stream/Cargo.lock +++ b/bindings/ruby/ext/ebur128_stream/Cargo.lock @@ -61,10 +61,21 @@ dependencies = [ "libloading", ] +[[package]] +name = "ebur128-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66e026ac32fee8397cdbaafee0030443af608d6c4832e590905b16c9450b7536" +dependencies = [ + "bitflags", + "libm", +] + [[package]] name = "ebur128_stream" version = "0.1.0" dependencies = [ + "ebur128-stream", "magnus", "rb-sys", "rb-sys-env", @@ -114,6 +125,12 @@ dependencies = [ "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" From 2c82c4a82fb90e864be0351e29c03ebe43a10992 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Wed, 12 Aug 2026 04:09:31 +0900 Subject: [PATCH 017/122] feat(ruby): update rb-sys-test-helpers --- bindings/ruby/ext/ebur128_stream/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/ruby/ext/ebur128_stream/Cargo.toml b/bindings/ruby/ext/ebur128_stream/Cargo.toml index f4cc019..36ca343 100644 --- a/bindings/ruby/ext/ebur128_stream/Cargo.toml +++ b/bindings/ruby/ext/ebur128_stream/Cargo.toml @@ -17,4 +17,4 @@ ebur128-stream = { version = "0.2.0" } rb-sys-env = "0.2.2" [dev-dependencies] -rb-sys-test-helpers = { version = "0.2.2" } +rb-sys-test-helpers = { version = "0.3.0" } From 945a4cf124c230ec6cb6c520a71df789604bee8f Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Wed, 12 Aug 2026 04:09:50 +0900 Subject: [PATCH 018/122] feat(ruby): install rb-sys-test-helpers --- bindings/ruby/ext/ebur128_stream/Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bindings/ruby/ext/ebur128_stream/Cargo.lock b/bindings/ruby/ext/ebur128_stream/Cargo.lock index f3c0962..7ec30b7 100644 --- a/bindings/ruby/ext/ebur128_stream/Cargo.lock +++ b/bindings/ruby/ext/ebur128_stream/Cargo.lock @@ -226,9 +226,9 @@ checksum = "cca7ad6a7e21e72151d56fe2495a259b5670e204c3adac41ee7ef676ea08117a" [[package]] name = "rb-sys-test-helpers" -version = "0.2.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6ccb543252549fc28f5d290322e041cd682bd199a8e1caaa813fb6e63dd221e" +checksum = "ccc637c53cad6a6b49ad51c5857c64e36bd70c955ffafe9dc9cc5bc9dd0b3b18" dependencies = [ "rb-sys", "rb-sys-env", @@ -237,9 +237,9 @@ dependencies = [ [[package]] name = "rb-sys-test-helpers-macros" -version = "0.2.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1508caed999cb659ab1b3308e7b2985186b3b550ef5492dc18da71b560c55615" +checksum = "b1c1fce35d9ac77a2745e539232fb82a75856675a5daf4ef8b1ca994d89f8b3f" dependencies = [ "quote", "syn", From ed14ca68557b048e251d9a1561db0f6d98fc851e Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Wed, 12 Aug 2026 07:21:28 +0900 Subject: [PATCH 019/122] feat(ruby): change crate name --- bindings/ruby/ext/ebur128_stream/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/ruby/ext/ebur128_stream/Cargo.toml b/bindings/ruby/ext/ebur128_stream/Cargo.toml index 36ca343..16df967 100644 --- a/bindings/ruby/ext/ebur128_stream/Cargo.toml +++ b/bindings/ruby/ext/ebur128_stream/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "ebur128_stream" +name = "ebur128-stream" version = "0.1.0" edition = "2021" authors = ["Kitaiti Makoto "] From 67441b3bd30d177deda2eab3145565c72c6ae542 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Wed, 12 Aug 2026 07:21:40 +0900 Subject: [PATCH 020/122] feat(ruby): change Rust edition --- bindings/ruby/ext/ebur128_stream/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/ruby/ext/ebur128_stream/Cargo.toml b/bindings/ruby/ext/ebur128_stream/Cargo.toml index 16df967..61d61fb 100644 --- a/bindings/ruby/ext/ebur128_stream/Cargo.toml +++ b/bindings/ruby/ext/ebur128_stream/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ebur128-stream" version = "0.1.0" -edition = "2021" +edition = "2024" authors = ["Kitaiti Makoto "] publish = false From dcd89c03f227cd84248f0025224240b532df8e6b Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Wed, 12 Aug 2026 07:22:04 +0900 Subject: [PATCH 021/122] ruby(feat): change dependency name --- bindings/ruby/ext/ebur128_stream/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/ruby/ext/ebur128_stream/Cargo.toml b/bindings/ruby/ext/ebur128_stream/Cargo.toml index 61d61fb..720169b 100644 --- a/bindings/ruby/ext/ebur128_stream/Cargo.toml +++ b/bindings/ruby/ext/ebur128_stream/Cargo.toml @@ -11,7 +11,7 @@ crate-type = ["cdylib"] [dependencies] magnus = { version = "0.8.2" } rb-sys = { version = "0.9", features = ["stable-api-compiled-fallback"] } -ebur128-stream = { version = "0.2.0" } +ebur128-stream-rs = { package = "ebur128-stream", version = "0.2.0" } [build-dependencies] rb-sys-env = "0.2.2" From bdeb557e9a920812feabfe3fd7dba00a51cf5627 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Wed, 12 Aug 2026 07:22:31 +0900 Subject: [PATCH 022/122] feat(ruby): update crates --- bindings/ruby/ext/ebur128_stream/Cargo.lock | 22 ++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/bindings/ruby/ext/ebur128_stream/Cargo.lock b/bindings/ruby/ext/ebur128_stream/Cargo.lock index 7ec30b7..db64f80 100644 --- a/bindings/ruby/ext/ebur128_stream/Cargo.lock +++ b/bindings/ruby/ext/ebur128_stream/Cargo.lock @@ -63,25 +63,25 @@ dependencies = [ [[package]] name = "ebur128-stream" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66e026ac32fee8397cdbaafee0030443af608d6c4832e590905b16c9450b7536" -dependencies = [ - "bitflags", - "libm", -] - -[[package]] -name = "ebur128_stream" version = "0.1.0" dependencies = [ - "ebur128-stream", + "ebur128-stream 0.2.0", "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", +] + [[package]] name = "either" version = "1.17.0" From a194f6a4fac85c71143e34bb3b0e8d83f434bdbf Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Wed, 12 Aug 2026 07:22:49 +0900 Subject: [PATCH 023/122] feat(ruby): implement Analyzer.new --- bindings/ruby/ext/ebur128_stream/src/lib.rs | 103 +++++++++++++++++--- 1 file changed, 87 insertions(+), 16 deletions(-) diff --git a/bindings/ruby/ext/ebur128_stream/src/lib.rs b/bindings/ruby/ext/ebur128_stream/src/lib.rs index 756a8a2..9a6fca7 100644 --- a/bindings/ruby/ext/ebur128_stream/src/lib.rs +++ b/bindings/ruby/ext/ebur128_stream/src/lib.rs @@ -1,23 +1,94 @@ -use magnus::{function, prelude::*, Error, Ruby}; +use core::time::Duration; +use ebur128_stream_rs::{Analyzer as AnalyzerRs, AnalyzerBuilder, Channel, Mode}; +use magnus::{ + Error, Integer, RArray, Ruby, Symbol, Value, function, method, + prelude::*, + scan_args::{get_kwargs, scan_args}, +}; -pub fn hello(subject: String) -> String { - format!("Hello {subject}, from Rust!") +#[magnus::wrap(class = "EBUR128Stream::Analyzer")] +struct Analyzer { + builder: Option, + analyzer: Option, } -#[magnus::init] -fn init(ruby: &Ruby) -> Result<(), Error> { - let module = ruby.define_module("EBUR128Stream")?; - module.define_singleton_method("hello", function!(hello, 1))?; - Ok(()) -} +impl Analyzer { + fn new(ruby: &Ruby, args: &[Value]) -> Result { + let args = scan_args::<(), (), (), (), _, ()>(args)?; + let kws = + get_kwargs::<_, (RArray,), (Option, Option, Option), ()>( + args.keywords, + &["channels"], + &["sample_rate", "modes", "expected_duration"], + )?; + let (channels,) = kws.required; + let (sample_rate, modes, expected_duration) = kws.optional; + + let channels: Vec = channels + .into_iter() + .map(|value| { + use Channel::*; + + let ch = Symbol::try_convert(value)?; + match ch.name()?.as_ref() { + "left" => Ok(Left), + "right" => Ok(Right), + "center" => Ok(Center), + "left_surround" => Ok(LeftSurround), + "right_surround" => Ok(RightSurround), + "lfe" => Ok(Lfe), + "other" => Ok(Other), + _ => Err(Error::new( + ruby.exception_arg_error(), + format!("unknown channel: {ch:?}"), + )), + } + }) + .collect::>()?; + + let mut builder = AnalyzerBuilder::new().channels(&channels); + + if let Some(sample_rate) = sample_rate { + builder = builder.sample_rate(sample_rate.to_u32()?); + } -#[cfg(test)] -mod tests { - use rb_sys_test_helpers::ruby_test; - use super::hello; + if let Some(values) = modes { + let mut modes = Mode::empty(); + for value in values.into_iter() { + let mode = Symbol::try_convert(value)?; + modes |= match mode.name()?.as_ref() { + "integrated" => Mode::Integrated, + "momentary" => Mode::Momentary, + "short_term" => Mode::ShortTerm, + "true_peak" => Mode::TruePeak, + "lra" => Mode::Lra, + "all" => Mode::All, + _ => return Err(Error::new(ruby.exception_arg_error(), format!("unknown mode: {mode:?}"))) + } + } + builder = builder.modes(modes); + } + if let Some(expected_duration) = expected_duration { + builder = builder.expected_duration(Duration::from_secs(expected_duration.to_u64()?)); + } - #[ruby_test] - fn test_hello() { - assert_eq!("Hello world, from Rust!", hello("world".to_string())); + // TODO: Pend build() and call it just before calling push_xxx() + let analyzer = builder + .build() + .map_err(|err| Error::new(ruby.exception_runtime_error(), format!("{}", err)))?; + + Ok(Self { + builder: None, + analyzer: Some(analyzer), + }) } } + +#[magnus::init] +fn init(ruby: &Ruby) -> Result<(), Error> { + let ebur128_stream = ruby.define_module("EBUR128Stream")?; + let analyzer = ebur128_stream.define_class("Analyzer", ruby.class_object())?; + analyzer.define_singleton_method("new", function!(Analyzer::new, -1))?; + + Ok(()) +} From 2ff0fed23d5a7233cf6842f6139f11affe90643c Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Wed, 12 Aug 2026 07:22:59 +0900 Subject: [PATCH 024/122] feat(ruby): add test for Analyzer.new --- bindings/ruby/test/test_ebur128_stream.rb | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/bindings/ruby/test/test_ebur128_stream.rb b/bindings/ruby/test/test_ebur128_stream.rb index e1e5477..0a1efd4 100644 --- a/bindings/ruby/test/test_ebur128_stream.rb +++ b/bindings/ruby/test/test_ebur128_stream.rb @@ -9,7 +9,21 @@ class EBUR128StreamTest < Test::Unit::TestCase end end - test "something useful" do - assert_equal("expected", "actual") + test "new" do + assert_raise ArgumentError do + EBUR128Stream::Analyzer.new + end + assert_instance_of EBUR128Stream::Analyzer, EBUR128Stream::Analyzer.new(channels: [:left]) + assert_raise ArgumentError do + EBUR128Stream::Analyzer.new(channels: [:left, :nothing]) + end + assert_instance_of EBUR128Stream::Analyzer, EBUR128Stream::Analyzer.new(channels: [:center], sample_rate: 48_000) + assert_raise RuntimeError do + EBUR128Stream::Analyzer.new(channels: [:center], modes: []) + end + assert_instance_of EBUR128Stream::Analyzer, EBUR128Stream::Analyzer.new(channels: [:center], modes: [:integrated, :true_peak]) + assert_raise ArgumentError do + EBUR128Stream::Analyzer.new(channels: [:center], modes: [:unknown]) + end end end From c2d46264cd9dc1466a4c8cda96176175863e6dbd Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Wed, 12 Aug 2026 08:53:27 +0900 Subject: [PATCH 025/122] feat(ruby): implement Analyzer#push_interleaved --- bindings/ruby/ext/ebur128_stream/src/lib.rs | 55 +++++++++++++++++---- 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/bindings/ruby/ext/ebur128_stream/src/lib.rs b/bindings/ruby/ext/ebur128_stream/src/lib.rs index 9a6fca7..e428ff6 100644 --- a/bindings/ruby/ext/ebur128_stream/src/lib.rs +++ b/bindings/ruby/ext/ebur128_stream/src/lib.rs @@ -1,26 +1,26 @@ use core::time::Duration; use ebur128_stream_rs::{Analyzer as AnalyzerRs, AnalyzerBuilder, Channel, Mode}; use magnus::{ - Error, Integer, RArray, Ruby, Symbol, Value, function, method, + Error, Integer, RArray, Ruby, Symbol, TryConvert, Value, function, method, prelude::*, scan_args::{get_kwargs, scan_args}, }; +use std::cell::RefCell; #[magnus::wrap(class = "EBUR128Stream::Analyzer")] struct Analyzer { builder: Option, - analyzer: Option, + analyzer: RefCell>, } impl Analyzer { fn new(ruby: &Ruby, args: &[Value]) -> Result { let args = scan_args::<(), (), (), (), _, ()>(args)?; - let kws = - get_kwargs::<_, (RArray,), (Option, Option, Option), ()>( - args.keywords, - &["channels"], - &["sample_rate", "modes", "expected_duration"], - )?; + let kws = get_kwargs::<_, (RArray,), (Option, Option, Option), ()>( + args.keywords, + &["channels"], + &["sample_rate", "modes", "expected_duration"], + )?; let (channels,) = kws.required; let (sample_rate, modes, expected_duration) = kws.optional; @@ -63,7 +63,12 @@ impl Analyzer { "true_peak" => Mode::TruePeak, "lra" => Mode::Lra, "all" => Mode::All, - _ => return Err(Error::new(ruby.exception_arg_error(), format!("unknown mode: {mode:?}"))) + _ => { + return Err(Error::new( + ruby.exception_arg_error(), + format!("unknown mode: {mode:?}"), + )); + } } } builder = builder.modes(modes); @@ -79,9 +84,38 @@ impl Analyzer { Ok(Self { builder: None, - analyzer: Some(analyzer), + analyzer: RefCell::new(Some(analyzer)), }) } + + fn push_interleaved(ruby: &Ruby, rb_self: &Self, samples: Value) -> Result<(), Error> { + // TODO: Accept MemoryView producer + // TODO: Consider chunking instead of converting whole samples at once + let samples = if let Some(array) = RArray::from_value(samples) { + array + .into_iter() + .map(TryConvert::try_convert) + .collect::, Error>>()? + } else { + return Err(Error::new( + ruby.exception_arg_error(), + format!("unsupported samples type: {samples:?}"), + )); + }; + + let mut analyzer = rb_self + .analyzer + .try_borrow_mut() + .map_err(|_| Error::new(ruby.exception_runtime_error(), "analyser already in use"))?; + let analyzer = analyzer.as_mut().ok_or_else(|| { + Error::new(ruby.exception_runtime_error(), "analyzer not initialized") + })?; + analyzer.push_interleaved(&samples).map_err(|_| { + Error::new(ruby.exception_runtime_error(), "failed to push interleaved") + })?; + + Ok(()) + } } #[magnus::init] @@ -89,6 +123,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> { let ebur128_stream = ruby.define_module("EBUR128Stream")?; let analyzer = ebur128_stream.define_class("Analyzer", ruby.class_object())?; analyzer.define_singleton_method("new", function!(Analyzer::new, -1))?; + analyzer.define_method("push_interleaved", method!(Analyzer::push_interleaved, 1))?; Ok(()) } From 788c8c8f688108fcd42a4a54e9aa7da95f71570f Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Wed, 12 Aug 2026 08:53:51 +0900 Subject: [PATCH 026/122] feat(ruby): add test for Analyzer#push_interleaved --- bindings/ruby/test/test_ebur128_stream.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/bindings/ruby/test/test_ebur128_stream.rb b/bindings/ruby/test/test_ebur128_stream.rb index 0a1efd4..97c7635 100644 --- a/bindings/ruby/test/test_ebur128_stream.rb +++ b/bindings/ruby/test/test_ebur128_stream.rb @@ -26,4 +26,11 @@ class EBUR128StreamTest < Test::Unit::TestCase EBUR128Stream::Analyzer.new(channels: [:center], modes: [:unknown]) end end + + test "push_interleaved" do + analyzer = EBUR128Stream::Analyzer.new(channels: [:left, :right], modes: [:all]) + assert_nothing_raised do + analyzer.push_interleaved [1.0, 1.0, 2.0, 2.0, 3.0, 3.0] + end + end end From 6473989f035ae7376548181ab31564b4c5150214 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Wed, 12 Aug 2026 09:02:38 +0900 Subject: [PATCH 027/122] feat(ruby): add Report --- bindings/ruby/ext/ebur128_stream/src/lib.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/bindings/ruby/ext/ebur128_stream/src/lib.rs b/bindings/ruby/ext/ebur128_stream/src/lib.rs index e428ff6..3164679 100644 --- a/bindings/ruby/ext/ebur128_stream/src/lib.rs +++ b/bindings/ruby/ext/ebur128_stream/src/lib.rs @@ -1,5 +1,5 @@ use core::time::Duration; -use ebur128_stream_rs::{Analyzer as AnalyzerRs, AnalyzerBuilder, Channel, Mode}; +use ebur128_stream_rs::{Analyzer as AnalyzerRs, AnalyzerBuilder, Channel, Mode, Report as RsReport}; use magnus::{ Error, Integer, RArray, Ruby, Symbol, TryConvert, Value, function, method, prelude::*, @@ -116,6 +116,10 @@ impl Analyzer { Ok(()) } + +#[magnus::wrap(class = "EBUR128Stream::Report")] +struct Report { + report: RsReport, } #[magnus::init] @@ -124,6 +128,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> { let analyzer = ebur128_stream.define_class("Analyzer", ruby.class_object())?; analyzer.define_singleton_method("new", function!(Analyzer::new, -1))?; analyzer.define_method("push_interleaved", method!(Analyzer::push_interleaved, 1))?; + let report = ebur128_stream.define_class("Report", ruby.class_object())?; Ok(()) } From 9da27ca90030689b2d11521632848a023a301dfc Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Wed, 12 Aug 2026 09:03:05 +0900 Subject: [PATCH 028/122] feat(ruby): define Analyzer#finalize --- bindings/ruby/ext/ebur128_stream/src/lib.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/bindings/ruby/ext/ebur128_stream/src/lib.rs b/bindings/ruby/ext/ebur128_stream/src/lib.rs index 3164679..133ce13 100644 --- a/bindings/ruby/ext/ebur128_stream/src/lib.rs +++ b/bindings/ruby/ext/ebur128_stream/src/lib.rs @@ -117,6 +117,22 @@ impl Analyzer { Ok(()) } + fn push_planner() {} + + fn finalize(ruby: &Ruby, rb_self: &Self) -> Result { + let mut analyzer = rb_self + .analyzer + .try_borrow_mut() + .map_err(|_| Error::new(ruby.exception_runtime_error(), "analyzer already in use"))?; + let analyzer = analyzer.take().ok_or_else(|| { + Error::new(ruby.exception_runtime_error(), "analyzer already finalized") + })?; + let report = analyzer.finalize(); + + Ok(Report { report }) + } +} + #[magnus::wrap(class = "EBUR128Stream::Report")] struct Report { report: RsReport, @@ -128,6 +144,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> { let analyzer = ebur128_stream.define_class("Analyzer", ruby.class_object())?; analyzer.define_singleton_method("new", function!(Analyzer::new, -1))?; analyzer.define_method("push_interleaved", method!(Analyzer::push_interleaved, 1))?; + analyzer.define_method("finalize", method!(Analyzer::finalize, 0))?; let report = ebur128_stream.define_class("Report", ruby.class_object())?; Ok(()) From a5d518ed18e7b1e1b6c1e8d8269a417f9f3e3ffe Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Wed, 12 Aug 2026 09:03:19 +0900 Subject: [PATCH 029/122] feat(ruby): add test for Analyzer#finalize --- bindings/ruby/test/test_ebur128_stream.rb | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/bindings/ruby/test/test_ebur128_stream.rb b/bindings/ruby/test/test_ebur128_stream.rb index 97c7635..9bbb225 100644 --- a/bindings/ruby/test/test_ebur128_stream.rb +++ b/bindings/ruby/test/test_ebur128_stream.rb @@ -33,4 +33,17 @@ class EBUR128StreamTest < Test::Unit::TestCase analyzer.push_interleaved [1.0, 1.0, 2.0, 2.0, 3.0, 3.0] end end + + test "finalize" do + analyzer = EBUR128Stream::Analyzer.new(channels: [:left, :right], modes: [:all]) + analyzer.push_interleaved [1.0, 1.0, 2.0, 2.0, 3.0, 3.0] + report = nil + assert_nothing_raised do + report = analyzer.finalize + end + assert_instance_of EBUR128Stream::Report, report + assert_raise_with_message RuntimeError, /finalized/ do + analyzer.finalize + end + end end From c2329bc950b4cd0d68fefd3243c3ce48cc38597e Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Wed, 12 Aug 2026 18:21:38 +0900 Subject: [PATCH 030/122] feat(ruby): define Analyzer#push_planar --- bindings/ruby/ext/ebur128_stream/src/lib.rs | 50 ++++++++++++++++++++- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/bindings/ruby/ext/ebur128_stream/src/lib.rs b/bindings/ruby/ext/ebur128_stream/src/lib.rs index 133ce13..ca48b09 100644 --- a/bindings/ruby/ext/ebur128_stream/src/lib.rs +++ b/bindings/ruby/ext/ebur128_stream/src/lib.rs @@ -1,5 +1,7 @@ use core::time::Duration; -use ebur128_stream_rs::{Analyzer as AnalyzerRs, AnalyzerBuilder, Channel, Mode, Report as RsReport}; +use ebur128_stream_rs::{ + Analyzer as AnalyzerRs, AnalyzerBuilder, Channel, Error as RsError, Mode, Report as RsReport, +}; use magnus::{ Error, Integer, RArray, Ruby, Symbol, TryConvert, Value, function, method, prelude::*, @@ -117,7 +119,50 @@ impl Analyzer { Ok(()) } - fn push_planner() {} + fn push_planar(ruby: &Ruby, rb_self: &Self, samples: Value) -> Result<(), Error> { + let samples: Vec> = if let Some(channels) = RArray::from_value(samples) { + channels + .into_iter() + .map(|channel| { + let ch = RArray::from_value(channel).ok_or_else(|| { + Error::new( + ruby.exception_arg_error(), + format!("channel not Array: {channel:?}"), + ) + })?; + ch.into_iter() + .map(f32::try_convert) + .collect::, Error>>() + }) + .collect::>, Error>>()? + } else { + return Err(Error::new( + ruby.exception_arg_error(), + format!("unsupported samples type: {samples:?}"), + )); + }; + let samples: Vec<&[f32]> = samples.iter().map(|channel| &channel[..]).collect(); + + let mut analyzer = rb_self + .analyzer + .try_borrow_mut() + .map_err(|_| Error::new(ruby.exception_runtime_error(), "analyzer already in use"))?; + let analyzer = analyzer.as_mut().ok_or_else(|| { + Error::new(ruby.exception_runtime_error(), "analyzer not initialized") + })?; + analyzer.push_planar(&samples).map_err(|err| match err { + RsError::ChannelMismatch { expected: _, got: _ } => { + Error::new(ruby.exception_arg_error(), format!("{err:?}")) + } + RsError::PlanarLengthMismatch { first: _, got: _ } => { + Error::new(ruby.exception_arg_error(), format!("{err:?}")) + } + RsError::NonFiniteSample => Error::new(ruby.exception_arg_error(), format!("{err:?}")), + _ => unreachable!(), + })?; + + Ok(()) + } fn finalize(ruby: &Ruby, rb_self: &Self) -> Result { let mut analyzer = rb_self @@ -144,6 +189,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> { let analyzer = ebur128_stream.define_class("Analyzer", ruby.class_object())?; analyzer.define_singleton_method("new", function!(Analyzer::new, -1))?; analyzer.define_method("push_interleaved", method!(Analyzer::push_interleaved, 1))?; + analyzer.define_method("push_planar", method!(Analyzer::push_planar, 1))?; analyzer.define_method("finalize", method!(Analyzer::finalize, 0))?; let report = ebur128_stream.define_class("Report", ruby.class_object())?; From 7bb795c3e0695695ca407875a5a8f9fe49cf3825 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Wed, 12 Aug 2026 18:21:48 +0900 Subject: [PATCH 031/122] feat(ruby): add test for Analyzer#push_planar --- bindings/ruby/test/test_ebur128_stream.rb | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/bindings/ruby/test/test_ebur128_stream.rb b/bindings/ruby/test/test_ebur128_stream.rb index 9bbb225..543d266 100644 --- a/bindings/ruby/test/test_ebur128_stream.rb +++ b/bindings/ruby/test/test_ebur128_stream.rb @@ -34,6 +34,17 @@ class EBUR128StreamTest < Test::Unit::TestCase end end + test "push_planar" do + analyzer = EBUR128Stream::Analyzer.new(channels: [:left, :right], modes: [:all]) + assert_nothing_raised do + analyzer.push_planar [[1.0, 2.0, 3.0], [1.0, 2.0, 3.0]] + analyzer.push_planar [[1.0, 2.0, 3.0], [1.0, 2.0, 3.0]] + end + assert_raise ArgumentError do + analyzer.push_planar [[1.0, 2.0, 3.0], [1.0, 2.0, 3.0], [1.0, 2.0, 3.0]] + end + end + test "finalize" do analyzer = EBUR128Stream::Analyzer.new(channels: [:left, :right], modes: [:all]) analyzer.push_interleaved [1.0, 1.0, 2.0, 2.0, 3.0, 3.0] From 35e0a47c33ed6e57eaa097c5c2328b8df80d32f4 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Wed, 12 Aug 2026 18:28:35 +0900 Subject: [PATCH 032/122] fix(ruby): respect engine's error --- bindings/ruby/ext/ebur128_stream/src/lib.rs | 22 +++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/bindings/ruby/ext/ebur128_stream/src/lib.rs b/bindings/ruby/ext/ebur128_stream/src/lib.rs index ca48b09..db3b60b 100644 --- a/bindings/ruby/ext/ebur128_stream/src/lib.rs +++ b/bindings/ruby/ext/ebur128_stream/src/lib.rs @@ -112,9 +112,18 @@ impl Analyzer { let analyzer = analyzer.as_mut().ok_or_else(|| { Error::new(ruby.exception_runtime_error(), "analyzer not initialized") })?; - analyzer.push_interleaved(&samples).map_err(|_| { - Error::new(ruby.exception_runtime_error(), "failed to push interleaved") - })?; + analyzer + .push_interleaved(&samples) + .map_err(|err| match err { + RsError::InterleavedLengthNotMultiple { + samples: _, + channels: _, + } => Error::new(ruby.exception_arg_error(), format!("{err:?}")), + RsError::NonFiniteSample => { + Error::new(ruby.exception_arg_error(), format!("{err:?}")) + } + _ => unreachable!(), + })?; Ok(()) } @@ -151,9 +160,10 @@ impl Analyzer { Error::new(ruby.exception_runtime_error(), "analyzer not initialized") })?; analyzer.push_planar(&samples).map_err(|err| match err { - RsError::ChannelMismatch { expected: _, got: _ } => { - Error::new(ruby.exception_arg_error(), format!("{err:?}")) - } + RsError::ChannelMismatch { + expected: _, + got: _, + } => Error::new(ruby.exception_arg_error(), format!("{err:?}")), RsError::PlanarLengthMismatch { first: _, got: _ } => { Error::new(ruby.exception_arg_error(), format!("{err:?}")) } From c1c950ad87c1fd41cd2c5b5b4dcd52cf189df6f7 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Wed, 12 Aug 2026 18:28:47 +0900 Subject: [PATCH 033/122] feat(ruby): add test for Analyzer#push_interleaved --- bindings/ruby/test/test_ebur128_stream.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bindings/ruby/test/test_ebur128_stream.rb b/bindings/ruby/test/test_ebur128_stream.rb index 543d266..054ad05 100644 --- a/bindings/ruby/test/test_ebur128_stream.rb +++ b/bindings/ruby/test/test_ebur128_stream.rb @@ -32,6 +32,9 @@ class EBUR128StreamTest < Test::Unit::TestCase assert_nothing_raised do analyzer.push_interleaved [1.0, 1.0, 2.0, 2.0, 3.0, 3.0] end + assert_raise ArgumentError do + analyzer.push_interleaved [1.0, 1.0, 2.0] + end end test "push_planar" do From 3677f5727c05c16e98e8d436607b9d2523d0880e Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Wed, 12 Aug 2026 18:36:02 +0900 Subject: [PATCH 034/122] chore(ruby): set prefix for original ebur128-stream --- bindings/ruby/ext/ebur128_stream/src/lib.rs | 30 +++++++++++---------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/bindings/ruby/ext/ebur128_stream/src/lib.rs b/bindings/ruby/ext/ebur128_stream/src/lib.rs index db3b60b..aae1e63 100644 --- a/bindings/ruby/ext/ebur128_stream/src/lib.rs +++ b/bindings/ruby/ext/ebur128_stream/src/lib.rs @@ -1,7 +1,5 @@ use core::time::Duration; -use ebur128_stream_rs::{ - Analyzer as AnalyzerRs, AnalyzerBuilder, Channel, Error as RsError, Mode, Report as RsReport, -}; +use ebur128_stream_rs as engine; use magnus::{ Error, Integer, RArray, Ruby, Symbol, TryConvert, Value, function, method, prelude::*, @@ -11,8 +9,8 @@ use std::cell::RefCell; #[magnus::wrap(class = "EBUR128Stream::Analyzer")] struct Analyzer { - builder: Option, - analyzer: RefCell>, + builder: Option, + analyzer: RefCell>, } impl Analyzer { @@ -26,10 +24,10 @@ impl Analyzer { let (channels,) = kws.required; let (sample_rate, modes, expected_duration) = kws.optional; - let channels: Vec = channels + let channels: Vec = channels .into_iter() .map(|value| { - use Channel::*; + use engine::Channel::*; let ch = Symbol::try_convert(value)?; match ch.name()?.as_ref() { @@ -48,13 +46,15 @@ impl Analyzer { }) .collect::>()?; - let mut builder = AnalyzerBuilder::new().channels(&channels); + let mut builder = engine::AnalyzerBuilder::new().channels(&channels); if let Some(sample_rate) = sample_rate { builder = builder.sample_rate(sample_rate.to_u32()?); } if let Some(values) = modes { + use engine::Mode; + let mut modes = Mode::empty(); for value in values.into_iter() { let mode = Symbol::try_convert(value)?; @@ -115,11 +115,11 @@ impl Analyzer { analyzer .push_interleaved(&samples) .map_err(|err| match err { - RsError::InterleavedLengthNotMultiple { + engine::Error::InterleavedLengthNotMultiple { samples: _, channels: _, } => Error::new(ruby.exception_arg_error(), format!("{err:?}")), - RsError::NonFiniteSample => { + engine::Error::NonFiniteSample => { Error::new(ruby.exception_arg_error(), format!("{err:?}")) } _ => unreachable!(), @@ -160,14 +160,16 @@ impl Analyzer { Error::new(ruby.exception_runtime_error(), "analyzer not initialized") })?; analyzer.push_planar(&samples).map_err(|err| match err { - RsError::ChannelMismatch { + engine::Error::ChannelMismatch { expected: _, got: _, } => Error::new(ruby.exception_arg_error(), format!("{err:?}")), - RsError::PlanarLengthMismatch { first: _, got: _ } => { + engine::Error::PlanarLengthMismatch { first: _, got: _ } => { + Error::new(ruby.exception_arg_error(), format!("{err:?}")) + } + engine::Error::NonFiniteSample => { Error::new(ruby.exception_arg_error(), format!("{err:?}")) } - RsError::NonFiniteSample => Error::new(ruby.exception_arg_error(), format!("{err:?}")), _ => unreachable!(), })?; @@ -190,7 +192,7 @@ impl Analyzer { #[magnus::wrap(class = "EBUR128Stream::Report")] struct Report { - report: RsReport, + report: engine::Report, } #[magnus::init] From 1f853314081d8abf6a6fe1193e8159731d98f995 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Wed, 12 Aug 2026 18:38:40 +0900 Subject: [PATCH 035/122] chore(ruby): remove unnecessary member builder from Analyzer --- bindings/ruby/ext/ebur128_stream/src/lib.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/bindings/ruby/ext/ebur128_stream/src/lib.rs b/bindings/ruby/ext/ebur128_stream/src/lib.rs index aae1e63..084f301 100644 --- a/bindings/ruby/ext/ebur128_stream/src/lib.rs +++ b/bindings/ruby/ext/ebur128_stream/src/lib.rs @@ -9,7 +9,6 @@ use std::cell::RefCell; #[magnus::wrap(class = "EBUR128Stream::Analyzer")] struct Analyzer { - builder: Option, analyzer: RefCell>, } @@ -85,7 +84,6 @@ impl Analyzer { .map_err(|err| Error::new(ruby.exception_runtime_error(), format!("{}", err)))?; Ok(Self { - builder: None, analyzer: RefCell::new(Some(analyzer)), }) } From aacabad8fce3996fc6333968fbb50387161c55f7 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Wed, 12 Aug 2026 19:08:49 +0900 Subject: [PATCH 036/122] feat(ruby): extract report module --- bindings/ruby/ext/ebur128_stream/src/lib.rs | 11 +++++------ bindings/ruby/ext/ebur128_stream/src/report.rs | 13 +++++++++++++ 2 files changed, 18 insertions(+), 6 deletions(-) create mode 100644 bindings/ruby/ext/ebur128_stream/src/report.rs diff --git a/bindings/ruby/ext/ebur128_stream/src/lib.rs b/bindings/ruby/ext/ebur128_stream/src/lib.rs index 084f301..8e280ec 100644 --- a/bindings/ruby/ext/ebur128_stream/src/lib.rs +++ b/bindings/ruby/ext/ebur128_stream/src/lib.rs @@ -1,3 +1,6 @@ +mod report; + +use crate::report::Report; use core::time::Duration; use ebur128_stream_rs as engine; use magnus::{ @@ -188,11 +191,6 @@ impl Analyzer { } } -#[magnus::wrap(class = "EBUR128Stream::Report")] -struct Report { - report: engine::Report, -} - #[magnus::init] fn init(ruby: &Ruby) -> Result<(), Error> { let ebur128_stream = ruby.define_module("EBUR128Stream")?; @@ -201,7 +199,8 @@ fn init(ruby: &Ruby) -> Result<(), Error> { analyzer.define_method("push_interleaved", method!(Analyzer::push_interleaved, 1))?; analyzer.define_method("push_planar", method!(Analyzer::push_planar, 1))?; analyzer.define_method("finalize", method!(Analyzer::finalize, 0))?; - let report = ebur128_stream.define_class("Report", ruby.class_object())?; + + report::init(ruby, &ebur128_stream)?; Ok(()) } diff --git a/bindings/ruby/ext/ebur128_stream/src/report.rs b/bindings/ruby/ext/ebur128_stream/src/report.rs new file mode 100644 index 0000000..6c7b2ae --- /dev/null +++ b/bindings/ruby/ext/ebur128_stream/src/report.rs @@ -0,0 +1,13 @@ +use ebur128_stream_rs as engine; +use magnus::{Error, RModule, Ruby, prelude::*}; + +#[magnus::wrap(class = "EBUR128Stream::Report")] +pub(crate) struct Report { + pub(crate) report: engine::Report, +} + +pub(crate) fn init(ruby: &Ruby, module: &RModule) -> Result<(), Error> { + let report = module.define_class("Report", ruby.class_object())?; + + Ok(()) +} From f84f96581e782c0d73682f31e4fd9e2adad47a10 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Wed, 12 Aug 2026 20:07:52 +0900 Subject: [PATCH 037/122] feat(ruby): implement Report attributes --- .../ruby/ext/ebur128_stream/src/report.rs | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/bindings/ruby/ext/ebur128_stream/src/report.rs b/bindings/ruby/ext/ebur128_stream/src/report.rs index 6c7b2ae..7293985 100644 --- a/bindings/ruby/ext/ebur128_stream/src/report.rs +++ b/bindings/ruby/ext/ebur128_stream/src/report.rs @@ -1,13 +1,45 @@ use ebur128_stream_rs as engine; -use magnus::{Error, RModule, Ruby, prelude::*}; +use magnus::{Error, RModule, Ruby, prelude::*, method}; #[magnus::wrap(class = "EBUR128Stream::Report")] pub(crate) struct Report { pub(crate) report: engine::Report, } +impl Report { + fn integrated_lufs(rb_self: &Self) -> Option { + rb_self.report.integrated_lufs() + } + + fn loudness_range_lu(rb_self: &Self) -> Option { + rb_self.report.loudness_range_lu() + } + + fn true_peak_dbtp(rb_self: &Self) -> Option { + rb_self.report.true_peak_dbtp() + } + + fn momentary_max_lufs(rb_self: &Self) -> Option { + rb_self.report.momentary_max_lufs() + } + + fn short_term_max_lufs(rb_self: &Self) -> Option { + rb_self.report.short_term_max_lufs() + } + + fn programme_duration_seconds(rb_self: &Self) -> f64 { + rb_self.report.programme_duration_seconds() + } +} + pub(crate) fn init(ruby: &Ruby, module: &RModule) -> Result<(), Error> { let report = module.define_class("Report", ruby.class_object())?; + report.define_method("integrated_lufs", method!(Report::integrated_lufs, 0))?; + report.define_method("loudness_range_lu", method!(Report::loudness_range_lu, 0))?; + report.define_method("true_peak_dbtp", method!(Report::true_peak_dbtp, 0))?; + report.define_method("momentary_max_lufs", method!(Report::momentary_max_lufs, 0))?; + report.define_method("short_term_max_lufs", method!(Report::short_term_max_lufs, 0))?; + report.define_method("programme_duration_seconds", method!(Report::programme_duration_seconds, 0))?; Ok(()) } From a9becdcef7158c396775f1407ff65a5ff415eff3 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Wed, 12 Aug 2026 20:08:03 +0900 Subject: [PATCH 038/122] feat(ruby): add test for Report --- bindings/ruby/test/test_report.rb | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 bindings/ruby/test/test_report.rb diff --git a/bindings/ruby/test/test_report.rb b/bindings/ruby/test/test_report.rb new file mode 100644 index 0000000..9c529ee --- /dev/null +++ b/bindings/ruby/test/test_report.rb @@ -0,0 +1,23 @@ +require_relative "helper" + +class TestReport < Test::Unit::TestCase + def setup + sample_rate = 48_000 + analyzer = EBUR128Stream::Analyzer.new(channels: [:left, :right], sample_rate:) + frame = 2 * Math::PI / (sample_rate / 4) + sample_rate.times do |n| + value = Math.cos(frame * n) * 10 + analyzer.push_interleaved [value, value] + end + @report = analyzer.finalize + end + + def test_attributes + assert_instance_of Float, @report.integrated_lufs + assert_nil @report.loudness_range_lu + assert_instance_of Float, @report.true_peak_dbtp + assert_instance_of Float, @report.momentary_max_lufs + assert_nil @report.short_term_max_lufs + assert_equal 1.0, @report.programme_duration_seconds + end +end From e5eba5ef6c5b5034c16b1e267a8db707027c0ead Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Wed, 12 Aug 2026 20:30:36 +0900 Subject: [PATCH 039/122] feat(ruby): customize Report#inspect --- bindings/ruby/lib/ebur128_stream.rb | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/bindings/ruby/lib/ebur128_stream.rb b/bindings/ruby/lib/ebur128_stream.rb index b92d6ec..1b73198 100644 --- a/bindings/ruby/lib/ebur128_stream.rb +++ b/bindings/ruby/lib/ebur128_stream.rb @@ -5,5 +5,22 @@ module EBUR128Stream class Error < StandardError; end - # Your code goes here... + + class Report + ATTRS = [ + :integrated_lufs, + :loudness_range_lu, + :true_peak_dbtp, + :momentary_max_lufs, + :short_term_max_lufs, + :programme_duration_seconds + ] + + def inspect + "#<%{class} %{attrs}>" % { + class: self.class, + attrs: ATTRS.collect {|attr| "#{attr}=#{send(attr) || nil.inspect}"}.join(" ") + } + end + end end From 86584ce6e9c2678234272f83f225f385dfec4f01 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Wed, 12 Aug 2026 20:34:06 +0900 Subject: [PATCH 040/122] feat(ruby): define Report#deconstruct_keys --- bindings/ruby/lib/ebur128_stream.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bindings/ruby/lib/ebur128_stream.rb b/bindings/ruby/lib/ebur128_stream.rb index 1b73198..603af8c 100644 --- a/bindings/ruby/lib/ebur128_stream.rb +++ b/bindings/ruby/lib/ebur128_stream.rb @@ -16,6 +16,14 @@ class Report :programme_duration_seconds ] + def deconstruct_keys(keys = nil) + keys = ATTRS if keys.nil? + (keys & ATTRS).inject({}) {|deconstructed, attr| + deconstructed[attr] = send(attr) + deconstructed + } + end + def inspect "#<%{class} %{attrs}>" % { class: self.class, From 0282b6cd133e54142a9861c9912766a85e23b260 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Wed, 12 Aug 2026 20:35:42 +0900 Subject: [PATCH 041/122] chore(ruby): add test for Report#deconstruct_keys --- bindings/ruby/test/test_report.rb | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/bindings/ruby/test/test_report.rb b/bindings/ruby/test/test_report.rb index 9c529ee..595d065 100644 --- a/bindings/ruby/test/test_report.rb +++ b/bindings/ruby/test/test_report.rb @@ -20,4 +20,15 @@ def test_attributes assert_nil @report.short_term_max_lufs assert_equal 1.0, @report.programme_duration_seconds end + + def test_deconstruct_keys + deconstructed = @report.deconstruct_keys(nil) + + assert_instance_of Float, deconstructed[:integrated_lufs] + assert_nil deconstructed[:loudness_range_lu] + assert_instance_of Float, deconstructed[:true_peak_dbtp] + assert_instance_of Float, deconstructed[:momentary_max_lufs] + assert_nil deconstructed[:short_term_max_lufs] + assert_equal 1.0, deconstructed[:programme_duration_seconds] + end end From c2bc17a3b6eefddd7d1103c5d7a063e76c39787b Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Wed, 12 Aug 2026 20:38:27 +0900 Subject: [PATCH 042/122] chore(ruby): run cargo fmt --- bindings/ruby/ext/ebur128_stream/src/report.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/bindings/ruby/ext/ebur128_stream/src/report.rs b/bindings/ruby/ext/ebur128_stream/src/report.rs index 7293985..c71d569 100644 --- a/bindings/ruby/ext/ebur128_stream/src/report.rs +++ b/bindings/ruby/ext/ebur128_stream/src/report.rs @@ -1,5 +1,5 @@ use ebur128_stream_rs as engine; -use magnus::{Error, RModule, Ruby, prelude::*, method}; +use magnus::{Error, RModule, Ruby, method, prelude::*}; #[magnus::wrap(class = "EBUR128Stream::Report")] pub(crate) struct Report { @@ -38,8 +38,14 @@ pub(crate) fn init(ruby: &Ruby, module: &RModule) -> Result<(), Error> { report.define_method("loudness_range_lu", method!(Report::loudness_range_lu, 0))?; report.define_method("true_peak_dbtp", method!(Report::true_peak_dbtp, 0))?; report.define_method("momentary_max_lufs", method!(Report::momentary_max_lufs, 0))?; - report.define_method("short_term_max_lufs", method!(Report::short_term_max_lufs, 0))?; - report.define_method("programme_duration_seconds", method!(Report::programme_duration_seconds, 0))?; + report.define_method( + "short_term_max_lufs", + method!(Report::short_term_max_lufs, 0), + )?; + report.define_method( + "programme_duration_seconds", + method!(Report::programme_duration_seconds, 0), + )?; Ok(()) } From 7cfea5dd386f4413c8eec76f39a2212ee1e62cf1 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Wed, 12 Aug 2026 20:59:30 +0900 Subject: [PATCH 043/122] feat(ruby): don't use unreachable! --- bindings/ruby/ext/ebur128_stream/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/ruby/ext/ebur128_stream/src/lib.rs b/bindings/ruby/ext/ebur128_stream/src/lib.rs index 8e280ec..7cdf477 100644 --- a/bindings/ruby/ext/ebur128_stream/src/lib.rs +++ b/bindings/ruby/ext/ebur128_stream/src/lib.rs @@ -171,7 +171,7 @@ impl Analyzer { engine::Error::NonFiniteSample => { Error::new(ruby.exception_arg_error(), format!("{err:?}")) } - _ => unreachable!(), + _ => Error::new(ruby.exception_runtime_error(), format!("{err:?}")), })?; Ok(()) From 5226c851b4b1913ec0b305eb482a6282410a3cb8 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Wed, 12 Aug 2026 21:07:50 +0900 Subject: [PATCH 044/122] chore(ruby): refine error messages --- bindings/ruby/ext/ebur128_stream/src/lib.rs | 26 ++++++++++----------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/bindings/ruby/ext/ebur128_stream/src/lib.rs b/bindings/ruby/ext/ebur128_stream/src/lib.rs index 7cdf477..151becc 100644 --- a/bindings/ruby/ext/ebur128_stream/src/lib.rs +++ b/bindings/ruby/ext/ebur128_stream/src/lib.rs @@ -42,7 +42,7 @@ impl Analyzer { "other" => Ok(Other), _ => Err(Error::new( ruby.exception_arg_error(), - format!("unknown channel: {ch:?}"), + format!("unknown channel: {ch}"), )), } }) @@ -70,7 +70,7 @@ impl Analyzer { _ => { return Err(Error::new( ruby.exception_arg_error(), - format!("unknown mode: {mode:?}"), + format!("unknown mode: {mode}"), )); } } @@ -84,7 +84,7 @@ impl Analyzer { // TODO: Pend build() and call it just before calling push_xxx() let analyzer = builder .build() - .map_err(|err| Error::new(ruby.exception_runtime_error(), format!("{}", err)))?; + .map_err(|err| Error::new(ruby.exception_runtime_error(), format!("{err}")))?; Ok(Self { analyzer: RefCell::new(Some(analyzer)), @@ -102,14 +102,14 @@ impl Analyzer { } else { return Err(Error::new( ruby.exception_arg_error(), - format!("unsupported samples type: {samples:?}"), + format!("unsupported samples type: {samples}"), )); }; let mut analyzer = rb_self .analyzer .try_borrow_mut() - .map_err(|_| Error::new(ruby.exception_runtime_error(), "analyser already in use"))?; + .map_err(|err| Error::new(ruby.exception_runtime_error(), format!("{err}")))?; let analyzer = analyzer.as_mut().ok_or_else(|| { Error::new(ruby.exception_runtime_error(), "analyzer not initialized") })?; @@ -119,9 +119,9 @@ impl Analyzer { engine::Error::InterleavedLengthNotMultiple { samples: _, channels: _, - } => Error::new(ruby.exception_arg_error(), format!("{err:?}")), + } => Error::new(ruby.exception_arg_error(), format!("{err}")), engine::Error::NonFiniteSample => { - Error::new(ruby.exception_arg_error(), format!("{err:?}")) + Error::new(ruby.exception_arg_error(), format!("{err}")) } _ => unreachable!(), })?; @@ -137,7 +137,7 @@ impl Analyzer { let ch = RArray::from_value(channel).ok_or_else(|| { Error::new( ruby.exception_arg_error(), - format!("channel not Array: {channel:?}"), + format!("channel not Array: {channel}"), ) })?; ch.into_iter() @@ -148,7 +148,7 @@ impl Analyzer { } else { return Err(Error::new( ruby.exception_arg_error(), - format!("unsupported samples type: {samples:?}"), + format!("unsupported samples type: {samples}"), )); }; let samples: Vec<&[f32]> = samples.iter().map(|channel| &channel[..]).collect(); @@ -164,14 +164,14 @@ impl Analyzer { engine::Error::ChannelMismatch { expected: _, got: _, - } => Error::new(ruby.exception_arg_error(), format!("{err:?}")), + } => Error::new(ruby.exception_arg_error(), format!("{err}")), engine::Error::PlanarLengthMismatch { first: _, got: _ } => { - Error::new(ruby.exception_arg_error(), format!("{err:?}")) + Error::new(ruby.exception_arg_error(), format!("{err}")) } engine::Error::NonFiniteSample => { - Error::new(ruby.exception_arg_error(), format!("{err:?}")) + Error::new(ruby.exception_arg_error(), format!("{err}")) } - _ => Error::new(ruby.exception_runtime_error(), format!("{err:?}")), + _ => Error::new(ruby.exception_runtime_error(), format!("{err}")), })?; Ok(()) From 735da05dad69968a99818530d804ff0b0edf18ea Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Wed, 12 Aug 2026 21:30:14 +0900 Subject: [PATCH 045/122] feat(ruby): enable ebur128-stream's normalize feature --- bindings/ruby/ext/ebur128_stream/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/ruby/ext/ebur128_stream/Cargo.toml b/bindings/ruby/ext/ebur128_stream/Cargo.toml index 720169b..51698f3 100644 --- a/bindings/ruby/ext/ebur128_stream/Cargo.toml +++ b/bindings/ruby/ext/ebur128_stream/Cargo.toml @@ -11,7 +11,7 @@ crate-type = ["cdylib"] [dependencies] magnus = { version = "0.8.2" } rb-sys = { version = "0.9", features = ["stable-api-compiled-fallback"] } -ebur128-stream-rs = { package = "ebur128-stream", version = "0.2.0" } +ebur128-stream-rs = { package = "ebur128-stream", version = "0.2.0", features= ["normalize"] } [build-dependencies] rb-sys-env = "0.2.2" From 2c274d2cff2f4fe8b53fc311e40a5b54ca70e937 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Thu, 13 Aug 2026 01:23:23 +0900 Subject: [PATCH 046/122] chore(ruby): extract parse_channels_arg --- bindings/ruby/ext/ebur128_stream/src/lib.rs | 50 ++++++++++++--------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/bindings/ruby/ext/ebur128_stream/src/lib.rs b/bindings/ruby/ext/ebur128_stream/src/lib.rs index 151becc..d9ca938 100644 --- a/bindings/ruby/ext/ebur128_stream/src/lib.rs +++ b/bindings/ruby/ext/ebur128_stream/src/lib.rs @@ -10,6 +10,33 @@ use magnus::{ }; use std::cell::RefCell; +pub(crate) fn parse_channels_arg( + ruby: &Ruby, + channels: RArray, +) -> Result, Error> { + use engine::Channel::*; + + channels + .into_iter() + .map(|value| { + let ch = Symbol::try_convert(value)?; + match ch.name()?.as_ref() { + "left" => Ok(Left), + "right" => Ok(Right), + "center" => Ok(Center), + "left_surround" => Ok(LeftSurround), + "right_surround" => Ok(RightSurround), + "lfe" => Ok(Lfe), + "other" => Ok(Other), + _ => Err(Error::new( + ruby.exception_arg_error(), + format!("unknown channel: {ch}"), + )), + } + }) + .collect() +} + #[magnus::wrap(class = "EBUR128Stream::Analyzer")] struct Analyzer { analyzer: RefCell>, @@ -26,28 +53,7 @@ impl Analyzer { let (channels,) = kws.required; let (sample_rate, modes, expected_duration) = kws.optional; - let channels: Vec = channels - .into_iter() - .map(|value| { - use engine::Channel::*; - - let ch = Symbol::try_convert(value)?; - match ch.name()?.as_ref() { - "left" => Ok(Left), - "right" => Ok(Right), - "center" => Ok(Center), - "left_surround" => Ok(LeftSurround), - "right_surround" => Ok(RightSurround), - "lfe" => Ok(Lfe), - "other" => Ok(Other), - _ => Err(Error::new( - ruby.exception_arg_error(), - format!("unknown channel: {ch}"), - )), - } - }) - .collect::>()?; - + let channels = parse_channels_arg(ruby, channels)?; let mut builder = engine::AnalyzerBuilder::new().channels(&channels); if let Some(sample_rate) = sample_rate { From a8552e921d73e333ee9c77d818edb0cb40ac39a8 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Thu, 13 Aug 2026 01:30:48 +0900 Subject: [PATCH 047/122] feat(ruby): define Normalizer.new --- bindings/ruby/ext/ebur128_stream/src/lib.rs | 3 ++ .../ruby/ext/ebur128_stream/src/normalize.rs | 45 +++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 bindings/ruby/ext/ebur128_stream/src/normalize.rs diff --git a/bindings/ruby/ext/ebur128_stream/src/lib.rs b/bindings/ruby/ext/ebur128_stream/src/lib.rs index d9ca938..49c6e3d 100644 --- a/bindings/ruby/ext/ebur128_stream/src/lib.rs +++ b/bindings/ruby/ext/ebur128_stream/src/lib.rs @@ -1,3 +1,4 @@ +mod normalize; mod report; use crate::report::Report; @@ -10,6 +11,7 @@ use magnus::{ }; use std::cell::RefCell; +// can make channels a slice? pub(crate) fn parse_channels_arg( ruby: &Ruby, channels: RArray, @@ -207,6 +209,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> { analyzer.define_method("finalize", method!(Analyzer::finalize, 0))?; report::init(ruby, &ebur128_stream)?; + normalize::init(ruby, &ebur128_stream)?; Ok(()) } diff --git a/bindings/ruby/ext/ebur128_stream/src/normalize.rs b/bindings/ruby/ext/ebur128_stream/src/normalize.rs new file mode 100644 index 0000000..c3441da --- /dev/null +++ b/bindings/ruby/ext/ebur128_stream/src/normalize.rs @@ -0,0 +1,45 @@ +use crate::parse_channels_arg; +use ebur128_stream_rs as engine; +use magnus::{ + Error, RArray, RModule, Ruby, Value, function, + prelude::*, + scan_args::{get_kwargs, scan_args}, +}; + +// Members are the same to engine::normalize::Normalizer +#[magnus::wrap(class = "EBUR128Stream::Normalizer")] +struct Normalizer { + sample_rate: u32, + channels: Box<[engine::Channel]>, + target_lufs: Option, + true_peak_ceiling_dbtp: Option, +} + +impl Normalizer { + fn new(ruby: &Ruby, args: &[Value]) -> Result { + let args = scan_args::<(), (), (), (), _, ()>(args)?; + let kws = get_kwargs::<_, (u32, RArray), (Option, Option), ()>( + args.keywords, + &["sample_rate", "channels"], + &["target_lufs", "true_peak_ceiling_dbtp"], + )?; + let (sample_rate, channels) = kws.required; + let (target_lufs, true_peak_ceiling_dbtp) = kws.optional; + + let channels = parse_channels_arg(ruby, channels)?; + + Ok(Self { + sample_rate, + channels: channels.into_boxed_slice(), + target_lufs, + true_peak_ceiling_dbtp, + }) + } +} + +pub(crate) fn init(ruby: &Ruby, module: &RModule) -> Result<(), Error> { + let normalizer = module.define_class("Normalizer", ruby.class_object())?; + normalizer.define_singleton_method("new", function!(Normalizer::new, -1))?; + + Ok(()) +} From be67d2fb1dabae94d76c055a57578aa0f5c49f8d Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Thu, 13 Aug 2026 01:31:04 +0900 Subject: [PATCH 048/122] feat(ruby): add test for Normalizer.new --- bindings/ruby/test/test_normalizer.rb | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 bindings/ruby/test/test_normalizer.rb diff --git a/bindings/ruby/test/test_normalizer.rb b/bindings/ruby/test/test_normalizer.rb new file mode 100644 index 0000000..5144678 --- /dev/null +++ b/bindings/ruby/test/test_normalizer.rb @@ -0,0 +1,9 @@ +require_relative "helper" + +class TestNormalizer < Test::Unit::TestCase + include EBUR128Stream + + def test_new + assert_instance_of Normalizer, Normalizer.new(sample_rate: 48_000, channels: [:left, :right]) + end +end From 15f9985c56c6788f7906c4d04c76ac4932c9d1d7 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Thu, 13 Aug 2026 02:03:29 +0900 Subject: [PATCH 049/122] feat(ruby): add NormalizeReport --- .../ruby/ext/ebur128_stream/src/normalize.rs | 56 ++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/bindings/ruby/ext/ebur128_stream/src/normalize.rs b/bindings/ruby/ext/ebur128_stream/src/normalize.rs index c3441da..f713d0b 100644 --- a/bindings/ruby/ext/ebur128_stream/src/normalize.rs +++ b/bindings/ruby/ext/ebur128_stream/src/normalize.rs @@ -1,7 +1,7 @@ use crate::parse_channels_arg; use ebur128_stream_rs as engine; use magnus::{ - Error, RArray, RModule, Ruby, Value, function, + Error, RArray, RModule, Ruby, Value, function, method, prelude::*, scan_args::{get_kwargs, scan_args}, }; @@ -37,9 +37,63 @@ impl Normalizer { } } +#[magnus::wrap(class = "EBUR128Stream::NormalizeReport")] +struct NormalizeReport { + report: engine::normalize::NormalizeReport, +} + +impl NormalizeReport { + fn measured_integrated_lufs(rb_self: &Self) -> Option { + rb_self.report.measured_integrated_lufs + } + + fn measured_true_peak_dbtp(rb_self: &Self) -> Option { + rb_self.report.measured_true_peak_dbtp + } + + fn target_lufs(rb_self: &Self) -> f64 { + rb_self.report.target_lufs + } + + fn true_peak_ceiling_dbtp(rb_self: &Self) -> Option { + rb_self.report.true_peak_ceiling_dbtp + } + + fn applied_gain_db(rb_self: &Self) -> f64 { + rb_self.report.applied_gain_db + } + + fn limited_by_true_peak(rb_self: &Self) -> bool { + rb_self.report.limited_by_true_peak + } +} + pub(crate) fn init(ruby: &Ruby, module: &RModule) -> Result<(), Error> { let normalizer = module.define_class("Normalizer", ruby.class_object())?; normalizer.define_singleton_method("new", function!(Normalizer::new, -1))?; + let normalize_report = module.define_class("NormalizeReport", ruby.class_object())?; + normalize_report.define_method( + "measured_integrated_lufs", + method!(NormalizeReport::measured_integrated_lufs, 0), + )?; + normalize_report.define_method( + "measured_true_peak_dbtp", + method!(NormalizeReport::measured_true_peak_dbtp, 0), + )?; + normalize_report.define_method("target_lufs", method!(NormalizeReport::target_lufs, 0))?; + normalize_report.define_method( + "true_peak_ceiling_dbtp", + method!(NormalizeReport::true_peak_ceiling_dbtp, 0), + )?; + normalize_report.define_method( + "applied_gain_db", + method!(NormalizeReport::applied_gain_db, 0), + )?; + normalize_report.define_method( + "limited_by_true_peak", + method!(NormalizeReport::limited_by_true_peak, 0), + )?; + Ok(()) } From 1519d92c0240ddc739859e2ccbfb3c78d531437f Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Thu, 13 Aug 2026 03:15:59 +0900 Subject: [PATCH 050/122] feat(ruby): implement Normalizer#normalize_in_place --- bindings/ruby/ext/ebur128_stream/src/lib.rs | 1 + .../ruby/ext/ebur128_stream/src/normalize.rs | 31 ++++++++++++- .../ruby/ext/ebur128_stream/src/samples.rs | 45 +++++++++++++++++++ 3 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 bindings/ruby/ext/ebur128_stream/src/samples.rs diff --git a/bindings/ruby/ext/ebur128_stream/src/lib.rs b/bindings/ruby/ext/ebur128_stream/src/lib.rs index 49c6e3d..cb628fc 100644 --- a/bindings/ruby/ext/ebur128_stream/src/lib.rs +++ b/bindings/ruby/ext/ebur128_stream/src/lib.rs @@ -1,5 +1,6 @@ mod normalize; mod report; +mod samples; use crate::report::Report; use core::time::Duration; diff --git a/bindings/ruby/ext/ebur128_stream/src/normalize.rs b/bindings/ruby/ext/ebur128_stream/src/normalize.rs index f713d0b..66e4a09 100644 --- a/bindings/ruby/ext/ebur128_stream/src/normalize.rs +++ b/bindings/ruby/ext/ebur128_stream/src/normalize.rs @@ -1,7 +1,7 @@ -use crate::parse_channels_arg; +use crate::{parse_channels_arg, samples::InterleavedSamples}; use ebur128_stream_rs as engine; use magnus::{ - Error, RArray, RModule, Ruby, Value, function, method, + Error, RArray, RModule, Ruby, TryConvert, Value, function, method, prelude::*, scan_args::{get_kwargs, scan_args}, }; @@ -35,6 +35,29 @@ impl Normalizer { true_peak_ceiling_dbtp, }) } + + fn normalize_in_place( + ruby: &Ruby, + rb_self: &Self, + samples: Value, + ) -> Result { + let mut frames = InterleavedSamples::try_convert(samples)?; + let mut normalizer = + engine::normalize::Normalizer::new(rb_self.sample_rate, &rb_self.channels); + if let Some(target_lufs) = rb_self.target_lufs { + normalizer = normalizer.target_lufs(target_lufs); + } + if let Some(true_peak_ceiling_dbtp) = rb_self.true_peak_ceiling_dbtp { + normalizer = normalizer.true_peak_ceiling_dbtp(true_peak_ceiling_dbtp); + } + + let report = normalizer + .normalize_in_place(frames.as_mut_slice()) + .map_err(|err| Error::new(ruby.exception_runtime_error(), format!("{err}")))?; + frames.write_back_in_place()?; + + Ok(NormalizeReport { report }) + } } #[magnus::wrap(class = "EBUR128Stream::NormalizeReport")] @@ -71,6 +94,10 @@ impl NormalizeReport { pub(crate) fn init(ruby: &Ruby, module: &RModule) -> Result<(), Error> { let normalizer = module.define_class("Normalizer", ruby.class_object())?; normalizer.define_singleton_method("new", function!(Normalizer::new, -1))?; + normalizer.define_method( + "normalize_in_place", + method!(Normalizer::normalize_in_place, 1), + )?; let normalize_report = module.define_class("NormalizeReport", ruby.class_object())?; normalize_report.define_method( diff --git a/bindings/ruby/ext/ebur128_stream/src/samples.rs b/bindings/ruby/ext/ebur128_stream/src/samples.rs new file mode 100644 index 0000000..5ef2441 --- /dev/null +++ b/bindings/ruby/ext/ebur128_stream/src/samples.rs @@ -0,0 +1,45 @@ +use magnus::{Error, RArray, Ruby, TryConvert, Value}; + +pub(crate) enum InterleavedSamples { + Array { obj: RArray, samples: Vec }, + // TODO: MemoryView support +} + +impl TryConvert for InterleavedSamples { + fn try_convert(val: Value) -> Result { + let samples = if let Some(obj) = RArray::from_value(val) { + let samples = obj + .into_iter() + .map(TryConvert::try_convert) + .collect::, Error>>()?; + Self::Array { obj, samples } + } else { + let ruby = Ruby::get_with(val); + return Err(Error::new( + ruby.exception_arg_error(), + format!("unsupported samples type: {val}"), + )); + }; + + Ok(samples) + } +} + +impl InterleavedSamples { + pub(crate) fn as_mut_slice(&mut self) -> &mut [f32] { + match self { + InterleavedSamples::Array { obj: _, samples } => samples, + } + } + + pub(crate) fn write_back_in_place(self) -> Result<(), Error> { + match self { + InterleavedSamples::Array { obj, samples } => { + let ruby = Ruby::get_with(obj); + obj.replace(ruby.ary_from_vec(samples))?; + } + } + + Ok(()) + } +} From 3ec129bb12ee7b9967af4599a371256d7d42c1bb Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Thu, 13 Aug 2026 04:12:57 +0900 Subject: [PATCH 051/122] feat(ruby): add test for Normalizer#normalize_in_place --- bindings/ruby/test/test_normalizer.rb | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/bindings/ruby/test/test_normalizer.rb b/bindings/ruby/test/test_normalizer.rb index 5144678..032e810 100644 --- a/bindings/ruby/test/test_normalizer.rb +++ b/bindings/ruby/test/test_normalizer.rb @@ -6,4 +6,19 @@ class TestNormalizer < Test::Unit::TestCase def test_new assert_instance_of Normalizer, Normalizer.new(sample_rate: 48_000, channels: [:left, :right]) end + + def test_normalize_in_place + sample_rate = 48_000 + normalizer = Normalizer.new(channels: [:left, :right], sample_rate:) + frame = 2 * Math::PI / (sample_rate / 4) + samples = sample_rate.times.flat_map do |n| + value = Math.cos(frame * n) * 10 + end + before = samples.dup + normalizer.normalize_in_place(samples) + + assert do + samples != before + end + end end From 76d65402d19ef0b7ec9372c9e895d66286afcfab Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Thu, 13 Aug 2026 04:24:13 +0900 Subject: [PATCH 052/122] chore(ruby): extract Reportable module --- bindings/ruby/lib/ebur128_stream.rb | 30 ++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/bindings/ruby/lib/ebur128_stream.rb b/bindings/ruby/lib/ebur128_stream.rb index 603af8c..91a8cb1 100644 --- a/bindings/ruby/lib/ebur128_stream.rb +++ b/bindings/ruby/lib/ebur128_stream.rb @@ -6,19 +6,10 @@ module EBUR128Stream class Error < StandardError; end - class Report - ATTRS = [ - :integrated_lufs, - :loudness_range_lu, - :true_peak_dbtp, - :momentary_max_lufs, - :short_term_max_lufs, - :programme_duration_seconds - ] - + module Reportable def deconstruct_keys(keys = nil) - keys = ATTRS if keys.nil? - (keys & ATTRS).inject({}) {|deconstructed, attr| + keys = self.class::ATTRS if keys.nil? + (keys & self.class::ATTRS).inject({}) {|deconstructed, attr| deconstructed[attr] = send(attr) deconstructed } @@ -27,8 +18,21 @@ def deconstruct_keys(keys = nil) def inspect "#<%{class} %{attrs}>" % { class: self.class, - attrs: ATTRS.collect {|attr| "#{attr}=#{send(attr) || nil.inspect}"}.join(" ") + attrs: self.class::ATTRS.collect {|attr| "#{attr}=#{send(attr) || nil.inspect}"}.join(" ") } end end + + class Report + include Reportable + + ATTRS = [ + :integrated_lufs, + :loudness_range_lu, + :true_peak_dbtp, + :momentary_max_lufs, + :short_term_max_lufs, + :programme_duration_seconds + ] + end end From ebe87f82025b1fd6a1b901bc1094dd8ed6ad0ad1 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Thu, 13 Aug 2026 04:31:09 +0900 Subject: [PATCH 053/122] feat(ruby): add NormalizeReport#deconstruct_keys and #inspect --- bindings/ruby/lib/ebur128_stream.rb | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/bindings/ruby/lib/ebur128_stream.rb b/bindings/ruby/lib/ebur128_stream.rb index 91a8cb1..45529b8 100644 --- a/bindings/ruby/lib/ebur128_stream.rb +++ b/bindings/ruby/lib/ebur128_stream.rb @@ -35,4 +35,17 @@ class Report :programme_duration_seconds ] end + + class NormalizeReport + include Reportable + + ATTRS = [ + :measured_integrated_lufs, + :measured_true_peak_dbtp, + :target_lufs, + :true_peak_ceiling_dbtp, + :applied_gain_db, + :limited_by_true_peak, + ] + end end From 0cef907748463f1468307658ad0cb564f54cf14f Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Thu, 13 Aug 2026 04:31:31 +0900 Subject: [PATCH 054/122] feat(ruby): add test for NormalizeReport reportable --- bindings/ruby/test/test_normalize_report.rb | 35 +++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 bindings/ruby/test/test_normalize_report.rb diff --git a/bindings/ruby/test/test_normalize_report.rb b/bindings/ruby/test/test_normalize_report.rb new file mode 100644 index 0000000..b31d173 --- /dev/null +++ b/bindings/ruby/test/test_normalize_report.rb @@ -0,0 +1,35 @@ +require_relative "helper" + +class TestNormalizeReport < Test::Unit::TestCase + include EBUR128Stream + + def setup + sample_rate = 48_000 + normalizer = Normalizer.new(channels: [:left, :right], sample_rate:) + frame = 2 * Math::PI / (sample_rate / 4) + samples = sample_rate.times.flat_map do |n| + value = Math.cos(frame * n) * 10 + end + @report = normalizer.normalize_in_place(samples) + end + + def test_attributes + assert_instance_of Float, @report.measured_integrated_lufs + assert_instance_of Float, @report.measured_true_peak_dbtp + assert_instance_of Float, @report.target_lufs + assert_nil @report.true_peak_ceiling_dbtp + assert_instance_of Float, @report.applied_gain_db + assert_false @report.limited_by_true_peak + end + + def test_deconstruct_keys + deconstructed = @report.deconstruct_keys(nil) + + assert_instance_of Float, deconstructed[:measured_integrated_lufs] + assert_instance_of Float, deconstructed[:measured_true_peak_dbtp] + assert_instance_of Float, deconstructed[:target_lufs] + assert_nil deconstructed[:true_peak_ceiling_dbtp] + assert_instance_of Float, deconstructed[:applied_gain_db] + assert_false deconstructed[:limited_by_true_peak] + end +end From 5e2da4339075422ad10fbe2242850e3d799b4f8b Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Thu, 13 Aug 2026 04:33:22 +0900 Subject: [PATCH 055/122] chore(ruby): inspect false properly --- bindings/ruby/lib/ebur128_stream.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/ruby/lib/ebur128_stream.rb b/bindings/ruby/lib/ebur128_stream.rb index 45529b8..0c6cbae 100644 --- a/bindings/ruby/lib/ebur128_stream.rb +++ b/bindings/ruby/lib/ebur128_stream.rb @@ -18,7 +18,7 @@ def deconstruct_keys(keys = nil) def inspect "#<%{class} %{attrs}>" % { class: self.class, - attrs: self.class::ATTRS.collect {|attr| "#{attr}=#{send(attr) || nil.inspect}"}.join(" ") + attrs: self.class::ATTRS.collect {|attr| "#{attr}=#{send(attr).inspect}"}.join(" ") } end end From 67fb4ea6a2ec4aa54a654172b06bfd770cbedf56 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Thu, 13 Aug 2026 06:35:47 +0900 Subject: [PATCH 056/122] chore(ruby): extract MemoryView --- bindings/ruby/ext/ebur128_stream/src/lib.rs | 1 + .../ext/ebur128_stream/src/memory_view.rs | 82 +++++++++++++++++++ .../ruby/ext/ebur128_stream/src/samples.rs | 39 ++++++++- 3 files changed, 118 insertions(+), 4 deletions(-) create mode 100644 bindings/ruby/ext/ebur128_stream/src/memory_view.rs diff --git a/bindings/ruby/ext/ebur128_stream/src/lib.rs b/bindings/ruby/ext/ebur128_stream/src/lib.rs index cb628fc..e5b2262 100644 --- a/bindings/ruby/ext/ebur128_stream/src/lib.rs +++ b/bindings/ruby/ext/ebur128_stream/src/lib.rs @@ -1,3 +1,4 @@ +mod memory_view; mod normalize; mod report; mod samples; diff --git a/bindings/ruby/ext/ebur128_stream/src/memory_view.rs b/bindings/ruby/ext/ebur128_stream/src/memory_view.rs new file mode 100644 index 0000000..8853dcf --- /dev/null +++ b/bindings/ruby/ext/ebur128_stream/src/memory_view.rs @@ -0,0 +1,82 @@ +use magnus::{Error, Ruby, Value}; +use rb_sys::{VALUE, rb_memory_view_get, rb_memory_view_release, rb_memory_view_t}; +use std::{ffi::CStr, mem::MaybeUninit, slice}; + +pub struct MemoryView { + inner: rb_memory_view_t, +} + +impl Drop for MemoryView { + fn drop(&mut self) { + // SAFETY: This guard is generated from successfully allocated rb_memory_view_t and drop() is called only once so it's not released more than once + unsafe { + rb_memory_view_release(&mut self.inner); + } + } +} + +impl MemoryView { + pub fn get(obj: Value, flags: i32) -> Result { + let ruby = Ruby::get_with(obj); + + let obj = unsafe { std::mem::transmute::(obj) }; + let mut view = MaybeUninit::uninit(); + let result = unsafe { rb_memory_view_get(obj, view.as_mut_ptr(), flags) }; + if !result { + return Err(Error::new( + ruby.exception_runtime_error(), + "MemoryView not got", + )); + } + let view = unsafe { view.assume_init() }; + + Ok(Self { inner: view }) + } + + fn obj(&self) -> Value { + unsafe { std::mem::transmute::(self.inner.obj) } + } + + pub fn byte_size(&self) -> Result { + usize::try_from(self.inner.byte_size).map_err(|err| { + let obj = self.obj(); + let ruby = Ruby::get_with(obj); + Error::new(ruby.exception_runtime_error(), format!("{err}")) + }) + } + + pub fn is_readonly(&self) -> bool { + self.inner.readonly + } + + pub fn format(&self) -> Option<&str> { + if self.inner.format.is_null() { + None + } else { + unsafe { CStr::from_ptr(self.inner.format).to_str() }.ok() + } + } + + pub fn data_as_mut(&mut self) -> Result<&mut [T], Error> { + if self.is_readonly() { + let ruby = Ruby::get_with(self.obj()); + return Err(Error::new(ruby.exception_runtime_error(), "not mutable")); + } + let n_items = self.byte_size()? / size_of::(); + let data = self.inner.data; + if data.is_null() { + let ruby = Ruby::get_with(self.obj()); + return Err(Error::new(ruby.exception_runtime_error(), "data is NULL")); + } + let ptr = data.cast::(); + if !ptr.is_aligned() { + let ruby = Ruby::get_with(self.obj()); + return Err(Error::new( + ruby.exception_runtime_error(), + "data not aligned", + )); + } + + Ok(unsafe { slice::from_raw_parts_mut(ptr, n_items) }) + } +} diff --git a/bindings/ruby/ext/ebur128_stream/src/samples.rs b/bindings/ruby/ext/ebur128_stream/src/samples.rs index 5ef2441..acf3785 100644 --- a/bindings/ruby/ext/ebur128_stream/src/samples.rs +++ b/bindings/ruby/ext/ebur128_stream/src/samples.rs @@ -1,13 +1,20 @@ +use crate::memory_view::MemoryView; use magnus::{Error, RArray, Ruby, TryConvert, Value}; +use rb_sys::ruby_memory_view_flags::{ + RUBY_MEMORY_VIEW_ANY_CONTIGUOUS, RUBY_MEMORY_VIEW_SIMPLE, RUBY_MEMORY_VIEW_WRITABLE, +}; pub(crate) enum InterleavedSamples { Array { obj: RArray, samples: Vec }, - // TODO: MemoryView support + MemoryView { view: MemoryView }, } impl TryConvert for InterleavedSamples { fn try_convert(val: Value) -> Result { - let samples = if let Some(obj) = RArray::from_value(val) { + let memory_view = Self::try_consume_memory_view(val); + let samples = if let Some(memory_view) = memory_view { + memory_view + } else if let Some(obj) = RArray::from_value(val) { let samples = obj .into_iter() .map(TryConvert::try_convert) @@ -28,18 +35,42 @@ impl TryConvert for InterleavedSamples { impl InterleavedSamples { pub(crate) fn as_mut_slice(&mut self) -> &mut [f32] { match self { - InterleavedSamples::Array { obj: _, samples } => samples, + Self::Array { obj: _, samples } => samples, + Self::MemoryView { view } => view.data_as_mut().unwrap_or(&mut []), } } pub(crate) fn write_back_in_place(self) -> Result<(), Error> { match self { - InterleavedSamples::Array { obj, samples } => { + Self::Array { obj, samples } => { let ruby = Ruby::get_with(obj); obj.replace(ruby.ary_from_vec(samples))?; } + Self::MemoryView { view: _ } => {} } Ok(()) } + + fn try_consume_memory_view(val: Value) -> Option { + let view = MemoryView::get(val, RUBY_MEMORY_VIEW_SIMPLE as i32); + if let Ok(view) = view { + if !view.is_readonly() && view.format().is_some() && view.format().unwrap() == "f" { + // TODO: Check format more strictly(size, other expression) + return Some(Self::MemoryView { view }); + } + } + let view = MemoryView::get( + val, + RUBY_MEMORY_VIEW_WRITABLE as i32 | RUBY_MEMORY_VIEW_ANY_CONTIGUOUS as i32, + ); + if let Ok(view) = view { + if let Some(format) = view.format() { + if format == "f" { + return Some(Self::MemoryView { view }); + } + } + } + None + } } From 2cde8e74730b6ec924a98193d9941f9cdf3276f4 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Thu, 13 Aug 2026 12:54:49 +0900 Subject: [PATCH 057/122] feat(ruby): use custom error --- bindings/ruby/ext/ebur128_stream/src/error.rs | 43 ++++++++++ bindings/ruby/ext/ebur128_stream/src/lib.rs | 80 +++++++------------ .../ruby/ext/ebur128_stream/src/normalize.rs | 6 +- .../ruby/ext/ebur128_stream/src/report.rs | 3 +- .../ruby/ext/ebur128_stream/src/samples.rs | 10 +-- 5 files changed, 81 insertions(+), 61 deletions(-) create mode 100644 bindings/ruby/ext/ebur128_stream/src/error.rs diff --git a/bindings/ruby/ext/ebur128_stream/src/error.rs b/bindings/ruby/ext/ebur128_stream/src/error.rs new file mode 100644 index 0000000..a521aab --- /dev/null +++ b/bindings/ruby/ext/ebur128_stream/src/error.rs @@ -0,0 +1,43 @@ +use ebur128_stream_rs as engine; +use magnus::{Ruby, error::IntoError}; + +pub(crate) enum Error { + Magnus(magnus::Error), + Engine(engine::Error), +} + +impl From for Error { + fn from(err: magnus::Error) -> Self { + Self::Magnus(err) + } +} + +impl From for Error { + fn from(err: engine::Error) -> Self { + Self::Engine(err) + } +} + +impl IntoError for Error { + fn into_error(self, ruby: &Ruby) -> magnus::Error { + match self { + Self::Magnus(err) => err, + Self::Engine(err) => { + let err_class = match err { + engine::Error::InterleavedLengthNotMultiple { + samples: _, + channels: _, + } + | engine::Error::ChannelMismatch { + expected: _, + got: _, + } + | engine::Error::PlanarLengthMismatch { first: _, got: _ } + | engine::Error::NonFiniteSample => ruby.exception_arg_error(), + _ => ruby.exception_runtime_error(), + }; + magnus::Error::new(err_class, err.to_string()) + } + } + } +} diff --git a/bindings/ruby/ext/ebur128_stream/src/lib.rs b/bindings/ruby/ext/ebur128_stream/src/lib.rs index e5b2262..b76b367 100644 --- a/bindings/ruby/ext/ebur128_stream/src/lib.rs +++ b/bindings/ruby/ext/ebur128_stream/src/lib.rs @@ -1,13 +1,14 @@ +mod error; mod memory_view; mod normalize; mod report; mod samples; -use crate::report::Report; +use crate::{error::Error, report::Report}; use core::time::Duration; use ebur128_stream_rs as engine; use magnus::{ - Error, Integer, RArray, Ruby, Symbol, TryConvert, Value, function, method, + Integer, RArray, Ruby, Symbol, TryConvert, Value, function, method, prelude::*, scan_args::{get_kwargs, scan_args}, }; @@ -32,10 +33,10 @@ pub(crate) fn parse_channels_arg( "right_surround" => Ok(RightSurround), "lfe" => Ok(Lfe), "other" => Ok(Other), - _ => Err(Error::new( + _ => Err(magnus::Error::new( ruby.exception_arg_error(), format!("unknown channel: {ch}"), - )), + ))?, } }) .collect() @@ -78,10 +79,10 @@ impl Analyzer { "lra" => Mode::Lra, "all" => Mode::All, _ => { - return Err(Error::new( + return Err(magnus::Error::new( ruby.exception_arg_error(), format!("unknown mode: {mode}"), - )); + ))?; } } } @@ -94,7 +95,7 @@ impl Analyzer { // TODO: Pend build() and call it just before calling push_xxx() let analyzer = builder .build() - .map_err(|err| Error::new(ruby.exception_runtime_error(), format!("{err}")))?; + .map_err(|err| magnus::Error::new(ruby.exception_runtime_error(), format!("{err}")))?; Ok(Self { analyzer: RefCell::new(Some(analyzer)), @@ -108,33 +109,22 @@ impl Analyzer { array .into_iter() .map(TryConvert::try_convert) - .collect::, Error>>()? + .collect::, magnus::Error>>()? } else { - return Err(Error::new( + return Err(magnus::Error::new( ruby.exception_arg_error(), format!("unsupported samples type: {samples}"), - )); + ))?; }; let mut analyzer = rb_self .analyzer .try_borrow_mut() - .map_err(|err| Error::new(ruby.exception_runtime_error(), format!("{err}")))?; + .map_err(|err| magnus::Error::new(ruby.exception_runtime_error(), format!("{err}")))?; let analyzer = analyzer.as_mut().ok_or_else(|| { - Error::new(ruby.exception_runtime_error(), "analyzer not initialized") + magnus::Error::new(ruby.exception_runtime_error(), "analyzer not initialized") })?; - analyzer - .push_interleaved(&samples) - .map_err(|err| match err { - engine::Error::InterleavedLengthNotMultiple { - samples: _, - channels: _, - } => Error::new(ruby.exception_arg_error(), format!("{err}")), - engine::Error::NonFiniteSample => { - Error::new(ruby.exception_arg_error(), format!("{err}")) - } - _ => unreachable!(), - })?; + analyzer.push_interleaved(&samples)?; Ok(()) } @@ -145,55 +135,41 @@ impl Analyzer { .into_iter() .map(|channel| { let ch = RArray::from_value(channel).ok_or_else(|| { - Error::new( + magnus::Error::new( ruby.exception_arg_error(), format!("channel not Array: {channel}"), ) })?; ch.into_iter() .map(f32::try_convert) - .collect::, Error>>() + .collect::, magnus::Error>>() }) - .collect::>, Error>>()? + .collect::>, magnus::Error>>()? } else { - return Err(Error::new( + return Err(magnus::Error::new( ruby.exception_arg_error(), format!("unsupported samples type: {samples}"), - )); + ))?; }; let samples: Vec<&[f32]> = samples.iter().map(|channel| &channel[..]).collect(); - let mut analyzer = rb_self - .analyzer - .try_borrow_mut() - .map_err(|_| Error::new(ruby.exception_runtime_error(), "analyzer already in use"))?; - let analyzer = analyzer.as_mut().ok_or_else(|| { - Error::new(ruby.exception_runtime_error(), "analyzer not initialized") + let mut analyzer = rb_self.analyzer.try_borrow_mut().map_err(|_| { + magnus::Error::new(ruby.exception_runtime_error(), "analyzer already in use") })?; - analyzer.push_planar(&samples).map_err(|err| match err { - engine::Error::ChannelMismatch { - expected: _, - got: _, - } => Error::new(ruby.exception_arg_error(), format!("{err}")), - engine::Error::PlanarLengthMismatch { first: _, got: _ } => { - Error::new(ruby.exception_arg_error(), format!("{err}")) - } - engine::Error::NonFiniteSample => { - Error::new(ruby.exception_arg_error(), format!("{err}")) - } - _ => Error::new(ruby.exception_runtime_error(), format!("{err}")), + let analyzer = analyzer.as_mut().ok_or_else(|| { + magnus::Error::new(ruby.exception_runtime_error(), "analyzer not initialized") })?; + analyzer.push_planar(&samples)?; Ok(()) } fn finalize(ruby: &Ruby, rb_self: &Self) -> Result { - let mut analyzer = rb_self - .analyzer - .try_borrow_mut() - .map_err(|_| Error::new(ruby.exception_runtime_error(), "analyzer already in use"))?; + let mut analyzer = rb_self.analyzer.try_borrow_mut().map_err(|_| { + magnus::Error::new(ruby.exception_runtime_error(), "analyzer already in use") + })?; let analyzer = analyzer.take().ok_or_else(|| { - Error::new(ruby.exception_runtime_error(), "analyzer already finalized") + magnus::Error::new(ruby.exception_runtime_error(), "analyzer already finalized") })?; let report = analyzer.finalize(); diff --git a/bindings/ruby/ext/ebur128_stream/src/normalize.rs b/bindings/ruby/ext/ebur128_stream/src/normalize.rs index 66e4a09..c5bf0e7 100644 --- a/bindings/ruby/ext/ebur128_stream/src/normalize.rs +++ b/bindings/ruby/ext/ebur128_stream/src/normalize.rs @@ -1,7 +1,7 @@ -use crate::{parse_channels_arg, samples::InterleavedSamples}; +use crate::{error::Error, parse_channels_arg, samples::InterleavedSamples}; use ebur128_stream_rs as engine; use magnus::{ - Error, RArray, RModule, Ruby, TryConvert, Value, function, method, + RArray, RModule, Ruby, TryConvert, Value, function, method, prelude::*, scan_args::{get_kwargs, scan_args}, }; @@ -53,7 +53,7 @@ impl Normalizer { let report = normalizer .normalize_in_place(frames.as_mut_slice()) - .map_err(|err| Error::new(ruby.exception_runtime_error(), format!("{err}")))?; + .map_err(|err| magnus::Error::new(ruby.exception_runtime_error(), format!("{err}")))?; frames.write_back_in_place()?; Ok(NormalizeReport { report }) diff --git a/bindings/ruby/ext/ebur128_stream/src/report.rs b/bindings/ruby/ext/ebur128_stream/src/report.rs index c71d569..8aa4844 100644 --- a/bindings/ruby/ext/ebur128_stream/src/report.rs +++ b/bindings/ruby/ext/ebur128_stream/src/report.rs @@ -1,5 +1,6 @@ +use crate::error::Error; use ebur128_stream_rs as engine; -use magnus::{Error, RModule, Ruby, method, prelude::*}; +use magnus::{RModule, Ruby, method, prelude::*}; #[magnus::wrap(class = "EBUR128Stream::Report")] pub(crate) struct Report { diff --git a/bindings/ruby/ext/ebur128_stream/src/samples.rs b/bindings/ruby/ext/ebur128_stream/src/samples.rs index acf3785..8fd79a5 100644 --- a/bindings/ruby/ext/ebur128_stream/src/samples.rs +++ b/bindings/ruby/ext/ebur128_stream/src/samples.rs @@ -1,5 +1,5 @@ -use crate::memory_view::MemoryView; -use magnus::{Error, RArray, Ruby, TryConvert, Value}; +use crate::{error::Error, memory_view::MemoryView}; +use magnus::{RArray, Ruby, TryConvert, Value}; use rb_sys::ruby_memory_view_flags::{ RUBY_MEMORY_VIEW_ANY_CONTIGUOUS, RUBY_MEMORY_VIEW_SIMPLE, RUBY_MEMORY_VIEW_WRITABLE, }; @@ -10,7 +10,7 @@ pub(crate) enum InterleavedSamples { } impl TryConvert for InterleavedSamples { - fn try_convert(val: Value) -> Result { + fn try_convert(val: Value) -> Result { let memory_view = Self::try_consume_memory_view(val); let samples = if let Some(memory_view) = memory_view { memory_view @@ -18,11 +18,11 @@ impl TryConvert for InterleavedSamples { let samples = obj .into_iter() .map(TryConvert::try_convert) - .collect::, Error>>()?; + .collect::, magnus::Error>>()?; Self::Array { obj, samples } } else { let ruby = Ruby::get_with(val); - return Err(Error::new( + return Err(magnus::Error::new( ruby.exception_arg_error(), format!("unsupported samples type: {val}"), )); From 11e269afb8acfe00401d4cd842c5837a4d0b1c53 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Thu, 13 Aug 2026 13:19:11 +0900 Subject: [PATCH 058/122] chore(ruby): use InterleavedSamples in push_interleaved --- bindings/ruby/ext/ebur128_stream/src/lib.rs | 18 +++--------------- .../ext/ebur128_stream/src/memory_view.rs | 19 +++++++++++++++++++ .../ruby/ext/ebur128_stream/src/samples.rs | 7 +++++++ 3 files changed, 29 insertions(+), 15 deletions(-) diff --git a/bindings/ruby/ext/ebur128_stream/src/lib.rs b/bindings/ruby/ext/ebur128_stream/src/lib.rs index b76b367..494ebbb 100644 --- a/bindings/ruby/ext/ebur128_stream/src/lib.rs +++ b/bindings/ruby/ext/ebur128_stream/src/lib.rs @@ -4,7 +4,7 @@ mod normalize; mod report; mod samples; -use crate::{error::Error, report::Report}; +use crate::{error::Error, report::Report, samples::InterleavedSamples}; use core::time::Duration; use ebur128_stream_rs as engine; use magnus::{ @@ -103,19 +103,7 @@ impl Analyzer { } fn push_interleaved(ruby: &Ruby, rb_self: &Self, samples: Value) -> Result<(), Error> { - // TODO: Accept MemoryView producer - // TODO: Consider chunking instead of converting whole samples at once - let samples = if let Some(array) = RArray::from_value(samples) { - array - .into_iter() - .map(TryConvert::try_convert) - .collect::, magnus::Error>>()? - } else { - return Err(magnus::Error::new( - ruby.exception_arg_error(), - format!("unsupported samples type: {samples}"), - ))?; - }; + let samples = InterleavedSamples::try_convert(samples)?; let mut analyzer = rb_self .analyzer @@ -124,7 +112,7 @@ impl Analyzer { let analyzer = analyzer.as_mut().ok_or_else(|| { magnus::Error::new(ruby.exception_runtime_error(), "analyzer not initialized") })?; - analyzer.push_interleaved(&samples)?; + analyzer.push_interleaved(samples.as_slice())?; Ok(()) } diff --git a/bindings/ruby/ext/ebur128_stream/src/memory_view.rs b/bindings/ruby/ext/ebur128_stream/src/memory_view.rs index 8853dcf..9841f69 100644 --- a/bindings/ruby/ext/ebur128_stream/src/memory_view.rs +++ b/bindings/ruby/ext/ebur128_stream/src/memory_view.rs @@ -57,6 +57,25 @@ impl MemoryView { } } + pub fn data(&self) -> Result<&[T], Error> { + let n_items = self.byte_size()? / size_of::(); + let data = self.inner.data; + if data.is_null() { + let ruby = Ruby::get_with(self.obj()); + return Err(Error::new(ruby.exception_runtime_error(), "data is NULL")); + } + let ptr = data.cast::(); + if !ptr.is_aligned() { + let ruby = Ruby::get_with(self.obj()); + return Err(Error::new( + ruby.exception_runtime_error(), + "data not aligned", + )); + } + + Ok(unsafe { slice::from_raw_parts(ptr, n_items) }) + } + pub fn data_as_mut(&mut self) -> Result<&mut [T], Error> { if self.is_readonly() { let ruby = Ruby::get_with(self.obj()); diff --git a/bindings/ruby/ext/ebur128_stream/src/samples.rs b/bindings/ruby/ext/ebur128_stream/src/samples.rs index 8fd79a5..b12966e 100644 --- a/bindings/ruby/ext/ebur128_stream/src/samples.rs +++ b/bindings/ruby/ext/ebur128_stream/src/samples.rs @@ -33,6 +33,13 @@ impl TryConvert for InterleavedSamples { } impl InterleavedSamples { + pub(crate) fn as_slice(&self) -> &[f32] { + match self { + Self::Array { obj: _, samples } => samples, + Self::MemoryView { view } => view.data().unwrap_or(&[]), + } + } + pub(crate) fn as_mut_slice(&mut self) -> &mut [f32] { match self { Self::Array { obj: _, samples } => samples, From e1444f0011565af90dab381302a7913278820f9f Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Thu, 13 Aug 2026 13:19:30 +0900 Subject: [PATCH 059/122] chore(ruby): remove outdated comment --- bindings/ruby/ext/ebur128_stream/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/bindings/ruby/ext/ebur128_stream/src/lib.rs b/bindings/ruby/ext/ebur128_stream/src/lib.rs index 494ebbb..6ed6d04 100644 --- a/bindings/ruby/ext/ebur128_stream/src/lib.rs +++ b/bindings/ruby/ext/ebur128_stream/src/lib.rs @@ -92,7 +92,6 @@ impl Analyzer { builder = builder.expected_duration(Duration::from_secs(expected_duration.to_u64()?)); } - // TODO: Pend build() and call it just before calling push_xxx() let analyzer = builder .build() .map_err(|err| magnus::Error::new(ruby.exception_runtime_error(), format!("{err}")))?; From 6465322c64f720cb58d0b65b0abfb51af0ab79e6 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Thu, 13 Aug 2026 13:47:28 +0900 Subject: [PATCH 060/122] chore(ruby): use RArray.to_vec() --- bindings/ruby/ext/ebur128_stream/src/samples.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/bindings/ruby/ext/ebur128_stream/src/samples.rs b/bindings/ruby/ext/ebur128_stream/src/samples.rs index b12966e..887cdc7 100644 --- a/bindings/ruby/ext/ebur128_stream/src/samples.rs +++ b/bindings/ruby/ext/ebur128_stream/src/samples.rs @@ -15,10 +15,7 @@ impl TryConvert for InterleavedSamples { let samples = if let Some(memory_view) = memory_view { memory_view } else if let Some(obj) = RArray::from_value(val) { - let samples = obj - .into_iter() - .map(TryConvert::try_convert) - .collect::, magnus::Error>>()?; + let samples = obj.to_vec::()?; Self::Array { obj, samples } } else { let ruby = Ruby::get_with(val); From c18f575d90ec38bb6bb42b83395c95adf3f9e3b3 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Thu, 13 Aug 2026 13:52:13 +0900 Subject: [PATCH 061/122] chore(ruby): remove needless variable binding --- .../ruby/ext/ebur128_stream/src/samples.rs | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/bindings/ruby/ext/ebur128_stream/src/samples.rs b/bindings/ruby/ext/ebur128_stream/src/samples.rs index 887cdc7..d3dd8bf 100644 --- a/bindings/ruby/ext/ebur128_stream/src/samples.rs +++ b/bindings/ruby/ext/ebur128_stream/src/samples.rs @@ -11,21 +11,20 @@ pub(crate) enum InterleavedSamples { impl TryConvert for InterleavedSamples { fn try_convert(val: Value) -> Result { - let memory_view = Self::try_consume_memory_view(val); - let samples = if let Some(memory_view) = memory_view { - memory_view - } else if let Some(obj) = RArray::from_value(val) { - let samples = obj.to_vec::()?; - Self::Array { obj, samples } - } else { - let ruby = Ruby::get_with(val); - return Err(magnus::Error::new( - ruby.exception_arg_error(), - format!("unsupported samples type: {val}"), - )); - }; - - Ok(samples) + Ok( + if let Some(memory_view) = Self::try_consume_memory_view(val) { + memory_view + } else if let Some(obj) = RArray::from_value(val) { + let samples = obj.to_vec::()?; + Self::Array { obj, samples } + } else { + let ruby = Ruby::get_with(val); + return Err(magnus::Error::new( + ruby.exception_arg_error(), + format!("unsupported samples type: {val}"), + )); + }, + ) } } From 777c962b55384e1d449107853866b90af3a6d7af Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Fri, 14 Aug 2026 00:05:04 +0900 Subject: [PATCH 062/122] feat(ruby): introduce memory_view::Flags --- .../ext/ebur128_stream/src/memory_view.rs | 112 +++++++++++++++++- .../ruby/ext/ebur128_stream/src/samples.rs | 12 +- 2 files changed, 115 insertions(+), 9 deletions(-) diff --git a/bindings/ruby/ext/ebur128_stream/src/memory_view.rs b/bindings/ruby/ext/ebur128_stream/src/memory_view.rs index 9841f69..45718fb 100644 --- a/bindings/ruby/ext/ebur128_stream/src/memory_view.rs +++ b/bindings/ruby/ext/ebur128_stream/src/memory_view.rs @@ -1,7 +1,113 @@ use magnus::{Error, Ruby, Value}; -use rb_sys::{VALUE, rb_memory_view_get, rb_memory_view_release, rb_memory_view_t}; +use rb_sys::{ + VALUE, rb_memory_view_get, rb_memory_view_release, rb_memory_view_t, + ruby_memory_view_flags::{ + RUBY_MEMORY_VIEW_ANY_CONTIGUOUS, RUBY_MEMORY_VIEW_COLUMN_MAJOR, RUBY_MEMORY_VIEW_FORMAT, + RUBY_MEMORY_VIEW_INDIRECT, RUBY_MEMORY_VIEW_MULTI_DIMENSIONAL, RUBY_MEMORY_VIEW_ROW_MAJOR, + RUBY_MEMORY_VIEW_SIMPLE, RUBY_MEMORY_VIEW_STRIDES, RUBY_MEMORY_VIEW_WRITABLE, + }, +}; use std::{ffi::CStr, mem::MaybeUninit, slice}; +pub struct Flags(i32); + +impl From for i32 { + fn from(value: Flags) -> Self { + value.0 + } +} + +impl Flags { + pub fn simple() -> Self { + Self(RUBY_MEMORY_VIEW_SIMPLE as i32) + } + + pub fn writable() -> Self { + Self(RUBY_MEMORY_VIEW_WRITABLE as i32) + } + + pub fn format() -> Self { + Self(RUBY_MEMORY_VIEW_FORMAT as i32) + } + + pub fn multi_dimensional() -> Self { + Self(RUBY_MEMORY_VIEW_MULTI_DIMENSIONAL as i32) + } + + pub fn strides() -> Self { + Self(RUBY_MEMORY_VIEW_STRIDES as i32) + } + + pub fn row_major() -> Self { + Self(RUBY_MEMORY_VIEW_ROW_MAJOR as i32) + } + + pub fn column_major() -> Self { + Self(RUBY_MEMORY_VIEW_COLUMN_MAJOR as i32) + } + + pub fn any_contiguous() -> Self { + Self(RUBY_MEMORY_VIEW_ANY_CONTIGUOUS as i32) + } + + pub fn indirect() -> Self { + Self(RUBY_MEMORY_VIEW_INDIRECT as i32) + } +} + +pub trait FlagsChainable { + fn writable(self) -> Self; + fn format(self) -> Self; + fn multi_dimensional(self) -> Self; + fn strides(self) -> Self; + fn row_major(self) -> Self; + fn column_major(self) -> Self; + fn any_contiguous(self) -> Self; + fn indirect(self) -> Self; +} + +impl FlagsChainable for Flags { + fn writable(mut self) -> Self { + self.0 |= RUBY_MEMORY_VIEW_WRITABLE as i32; + self + } + + fn format(mut self) -> Self { + self.0 |= RUBY_MEMORY_VIEW_FORMAT as i32; + self + } + + fn multi_dimensional(mut self) -> Self { + self.0 |= RUBY_MEMORY_VIEW_MULTI_DIMENSIONAL as i32; + self + } + + fn strides(mut self) -> Self { + self.0 |= RUBY_MEMORY_VIEW_STRIDES as i32; + self + } + + fn row_major(mut self) -> Self { + self.0 |= RUBY_MEMORY_VIEW_ROW_MAJOR as i32; + self + } + + fn column_major(mut self) -> Self { + self.0 |= RUBY_MEMORY_VIEW_COLUMN_MAJOR as i32; + self + } + + fn any_contiguous(mut self) -> Self { + self.0 |= RUBY_MEMORY_VIEW_ANY_CONTIGUOUS as i32; + self + } + + fn indirect(mut self) -> Self { + self.0 |= RUBY_MEMORY_VIEW_INDIRECT as i32; + self + } +} + pub struct MemoryView { inner: rb_memory_view_t, } @@ -16,12 +122,12 @@ impl Drop for MemoryView { } impl MemoryView { - pub fn get(obj: Value, flags: i32) -> Result { + pub fn get(obj: Value, flags: Flags) -> Result { let ruby = Ruby::get_with(obj); let obj = unsafe { std::mem::transmute::(obj) }; let mut view = MaybeUninit::uninit(); - let result = unsafe { rb_memory_view_get(obj, view.as_mut_ptr(), flags) }; + let result = unsafe { rb_memory_view_get(obj, view.as_mut_ptr(), flags.into()) }; if !result { return Err(Error::new( ruby.exception_runtime_error(), diff --git a/bindings/ruby/ext/ebur128_stream/src/samples.rs b/bindings/ruby/ext/ebur128_stream/src/samples.rs index d3dd8bf..8d16b02 100644 --- a/bindings/ruby/ext/ebur128_stream/src/samples.rs +++ b/bindings/ruby/ext/ebur128_stream/src/samples.rs @@ -1,4 +1,7 @@ -use crate::{error::Error, memory_view::MemoryView}; +use crate::{ + error::Error, + memory_view::{Flags, FlagsChainable, MemoryView}, +}; use magnus::{RArray, Ruby, TryConvert, Value}; use rb_sys::ruby_memory_view_flags::{ RUBY_MEMORY_VIEW_ANY_CONTIGUOUS, RUBY_MEMORY_VIEW_SIMPLE, RUBY_MEMORY_VIEW_WRITABLE, @@ -56,17 +59,14 @@ impl InterleavedSamples { } fn try_consume_memory_view(val: Value) -> Option { - let view = MemoryView::get(val, RUBY_MEMORY_VIEW_SIMPLE as i32); + let view = MemoryView::get(val, Flags::simple()); if let Ok(view) = view { if !view.is_readonly() && view.format().is_some() && view.format().unwrap() == "f" { // TODO: Check format more strictly(size, other expression) return Some(Self::MemoryView { view }); } } - let view = MemoryView::get( - val, - RUBY_MEMORY_VIEW_WRITABLE as i32 | RUBY_MEMORY_VIEW_ANY_CONTIGUOUS as i32, - ); + let view = MemoryView::get(val, Flags::writable().any_contiguous()); if let Ok(view) = view { if let Some(format) = view.format() { if format == "f" { From 76efc4f025bde78f27ce24b5967fe88a9c53dd8d Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Fri, 14 Aug 2026 00:30:06 +0900 Subject: [PATCH 063/122] chore(ruby): introduce Error::Runtime and Argument --- bindings/ruby/ext/ebur128_stream/src/error.rs | 14 ++++ bindings/ruby/ext/ebur128_stream/src/lib.rs | 80 ++++++++----------- .../ruby/ext/ebur128_stream/src/normalize.rs | 12 +-- .../ruby/ext/ebur128_stream/src/samples.rs | 9 +-- 4 files changed, 54 insertions(+), 61 deletions(-) diff --git a/bindings/ruby/ext/ebur128_stream/src/error.rs b/bindings/ruby/ext/ebur128_stream/src/error.rs index a521aab..4211a19 100644 --- a/bindings/ruby/ext/ebur128_stream/src/error.rs +++ b/bindings/ruby/ext/ebur128_stream/src/error.rs @@ -4,6 +4,8 @@ use magnus::{Ruby, error::IntoError}; pub(crate) enum Error { Magnus(magnus::Error), Engine(engine::Error), + Runtime(String), + Argument(String), } impl From for Error { @@ -38,6 +40,18 @@ impl IntoError for Error { }; magnus::Error::new(err_class, err.to_string()) } + Self::Runtime(msg) => magnus::Error::new(ruby.exception_runtime_error(), msg), + Self::Argument(msg) => magnus::Error::new(ruby.exception_arg_error(), msg), } } } + +impl Error { + pub(crate) fn runtime(err: T) -> Self { + Self::Runtime(err.to_string()) + } + + pub(crate) fn argument(err: T) -> Self { + Self::Argument(err.to_string()) + } +} diff --git a/bindings/ruby/ext/ebur128_stream/src/lib.rs b/bindings/ruby/ext/ebur128_stream/src/lib.rs index 6ed6d04..97fc361 100644 --- a/bindings/ruby/ext/ebur128_stream/src/lib.rs +++ b/bindings/ruby/ext/ebur128_stream/src/lib.rs @@ -8,17 +8,16 @@ use crate::{error::Error, report::Report, samples::InterleavedSamples}; use core::time::Duration; use ebur128_stream_rs as engine; use magnus::{ - Integer, RArray, Ruby, Symbol, TryConvert, Value, function, method, + Integer, RArray, Ruby, Symbol, TryConvert, Value, + error::IntoError, + function, method, prelude::*, scan_args::{get_kwargs, scan_args}, }; use std::cell::RefCell; // can make channels a slice? -pub(crate) fn parse_channels_arg( - ruby: &Ruby, - channels: RArray, -) -> Result, Error> { +pub(crate) fn parse_channels_arg(channels: RArray) -> Result, Error> { use engine::Channel::*; channels @@ -33,10 +32,7 @@ pub(crate) fn parse_channels_arg( "right_surround" => Ok(RightSurround), "lfe" => Ok(Lfe), "other" => Ok(Other), - _ => Err(magnus::Error::new( - ruby.exception_arg_error(), - format!("unknown channel: {ch}"), - ))?, + _ => Err(Error::argument(format!("unknown channel: {ch}")))?, } }) .collect() @@ -48,7 +44,7 @@ struct Analyzer { } impl Analyzer { - fn new(ruby: &Ruby, args: &[Value]) -> Result { + fn new(args: &[Value]) -> Result { let args = scan_args::<(), (), (), (), _, ()>(args)?; let kws = get_kwargs::<_, (RArray,), (Option, Option, Option), ()>( args.keywords, @@ -58,7 +54,7 @@ impl Analyzer { let (channels,) = kws.required; let (sample_rate, modes, expected_duration) = kws.optional; - let channels = parse_channels_arg(ruby, channels)?; + let channels = parse_channels_arg(channels)?; let mut builder = engine::AnalyzerBuilder::new().channels(&channels); if let Some(sample_rate) = sample_rate { @@ -79,10 +75,7 @@ impl Analyzer { "lra" => Mode::Lra, "all" => Mode::All, _ => { - return Err(magnus::Error::new( - ruby.exception_arg_error(), - format!("unknown mode: {mode}"), - ))?; + return Err(Error::argument(format!("unknown mode: {mode}")))?; } } } @@ -92,25 +85,20 @@ impl Analyzer { builder = builder.expected_duration(Duration::from_secs(expected_duration.to_u64()?)); } - let analyzer = builder - .build() - .map_err(|err| magnus::Error::new(ruby.exception_runtime_error(), format!("{err}")))?; + let analyzer = builder.build().map_err(Error::runtime)?; Ok(Self { analyzer: RefCell::new(Some(analyzer)), }) } - fn push_interleaved(ruby: &Ruby, rb_self: &Self, samples: Value) -> Result<(), Error> { + fn push_interleaved(rb_self: &Self, samples: Value) -> Result<(), Error> { let samples = InterleavedSamples::try_convert(samples)?; - let mut analyzer = rb_self - .analyzer - .try_borrow_mut() - .map_err(|err| magnus::Error::new(ruby.exception_runtime_error(), format!("{err}")))?; - let analyzer = analyzer.as_mut().ok_or_else(|| { - magnus::Error::new(ruby.exception_runtime_error(), "analyzer not initialized") - })?; + let mut analyzer = rb_self.analyzer.try_borrow_mut().map_err(Error::runtime)?; + let analyzer = analyzer + .as_mut() + .ok_or_else(|| Error::runtime("analyzer not initialized"))?; analyzer.push_interleaved(samples.as_slice())?; Ok(()) @@ -122,10 +110,7 @@ impl Analyzer { .into_iter() .map(|channel| { let ch = RArray::from_value(channel).ok_or_else(|| { - magnus::Error::new( - ruby.exception_arg_error(), - format!("channel not Array: {channel}"), - ) + Error::argument(format!("channel not Array: {channel}")).into_error(ruby) })?; ch.into_iter() .map(f32::try_convert) @@ -133,31 +118,32 @@ impl Analyzer { }) .collect::>, magnus::Error>>()? } else { - return Err(magnus::Error::new( - ruby.exception_arg_error(), - format!("unsupported samples type: {samples}"), - ))?; + return Err(Error::argument(format!( + "unsupported samples type: {samples}" + )))?; }; let samples: Vec<&[f32]> = samples.iter().map(|channel| &channel[..]).collect(); - let mut analyzer = rb_self.analyzer.try_borrow_mut().map_err(|_| { - magnus::Error::new(ruby.exception_runtime_error(), "analyzer already in use") - })?; - let analyzer = analyzer.as_mut().ok_or_else(|| { - magnus::Error::new(ruby.exception_runtime_error(), "analyzer not initialized") - })?; + let mut analyzer = rb_self + .analyzer + .try_borrow_mut() + .map_err(|_| Error::runtime("analyzer already in use"))?; + let analyzer = analyzer + .as_mut() + .ok_or_else(|| Error::runtime("analyzer not initialized"))?; analyzer.push_planar(&samples)?; Ok(()) } - fn finalize(ruby: &Ruby, rb_self: &Self) -> Result { - let mut analyzer = rb_self.analyzer.try_borrow_mut().map_err(|_| { - magnus::Error::new(ruby.exception_runtime_error(), "analyzer already in use") - })?; - let analyzer = analyzer.take().ok_or_else(|| { - magnus::Error::new(ruby.exception_runtime_error(), "analyzer already finalized") - })?; + fn finalize(rb_self: &Self) -> Result { + let mut analyzer = rb_self + .analyzer + .try_borrow_mut() + .map_err(|_| Error::runtime("analyzer already in use"))?; + let analyzer = analyzer + .take() + .ok_or_else(|| Error::runtime("analyzer already finalized"))?; let report = analyzer.finalize(); Ok(Report { report }) diff --git a/bindings/ruby/ext/ebur128_stream/src/normalize.rs b/bindings/ruby/ext/ebur128_stream/src/normalize.rs index c5bf0e7..c9dc03e 100644 --- a/bindings/ruby/ext/ebur128_stream/src/normalize.rs +++ b/bindings/ruby/ext/ebur128_stream/src/normalize.rs @@ -16,7 +16,7 @@ struct Normalizer { } impl Normalizer { - fn new(ruby: &Ruby, args: &[Value]) -> Result { + fn new(args: &[Value]) -> Result { let args = scan_args::<(), (), (), (), _, ()>(args)?; let kws = get_kwargs::<_, (u32, RArray), (Option, Option), ()>( args.keywords, @@ -26,7 +26,7 @@ impl Normalizer { let (sample_rate, channels) = kws.required; let (target_lufs, true_peak_ceiling_dbtp) = kws.optional; - let channels = parse_channels_arg(ruby, channels)?; + let channels = parse_channels_arg(channels)?; Ok(Self { sample_rate, @@ -36,11 +36,7 @@ impl Normalizer { }) } - fn normalize_in_place( - ruby: &Ruby, - rb_self: &Self, - samples: Value, - ) -> Result { + fn normalize_in_place(rb_self: &Self, samples: Value) -> Result { let mut frames = InterleavedSamples::try_convert(samples)?; let mut normalizer = engine::normalize::Normalizer::new(rb_self.sample_rate, &rb_self.channels); @@ -53,7 +49,7 @@ impl Normalizer { let report = normalizer .normalize_in_place(frames.as_mut_slice()) - .map_err(|err| magnus::Error::new(ruby.exception_runtime_error(), format!("{err}")))?; + .map_err(Error::runtime)?; frames.write_back_in_place()?; Ok(NormalizeReport { report }) diff --git a/bindings/ruby/ext/ebur128_stream/src/samples.rs b/bindings/ruby/ext/ebur128_stream/src/samples.rs index 8d16b02..9d8e9e2 100644 --- a/bindings/ruby/ext/ebur128_stream/src/samples.rs +++ b/bindings/ruby/ext/ebur128_stream/src/samples.rs @@ -2,7 +2,7 @@ use crate::{ error::Error, memory_view::{Flags, FlagsChainable, MemoryView}, }; -use magnus::{RArray, Ruby, TryConvert, Value}; +use magnus::{RArray, Ruby, TryConvert, Value, error::IntoError}; use rb_sys::ruby_memory_view_flags::{ RUBY_MEMORY_VIEW_ANY_CONTIGUOUS, RUBY_MEMORY_VIEW_SIMPLE, RUBY_MEMORY_VIEW_WRITABLE, }; @@ -21,11 +21,8 @@ impl TryConvert for InterleavedSamples { let samples = obj.to_vec::()?; Self::Array { obj, samples } } else { - let ruby = Ruby::get_with(val); - return Err(magnus::Error::new( - ruby.exception_arg_error(), - format!("unsupported samples type: {val}"), - )); + return Err(Error::argument(format!("unsupported samples type: {val}")) + .into_error(&Ruby::get_with(val))); }, ) } From c9cc4bd046801570825332a8b8fd7961f8786e47 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Fri, 14 Aug 2026 02:15:23 +0900 Subject: [PATCH 064/122] feat(ruby): introduce PlanarSamples and support MemoryView for push_planar() --- bindings/ruby/ext/ebur128_stream/src/lib.rs | 34 ++++-------- .../ext/ebur128_stream/src/memory_view.rs | 4 ++ .../ruby/ext/ebur128_stream/src/samples.rs | 52 +++++++++++++++++++ 3 files changed, 65 insertions(+), 25 deletions(-) diff --git a/bindings/ruby/ext/ebur128_stream/src/lib.rs b/bindings/ruby/ext/ebur128_stream/src/lib.rs index 97fc361..0ada55c 100644 --- a/bindings/ruby/ext/ebur128_stream/src/lib.rs +++ b/bindings/ruby/ext/ebur128_stream/src/lib.rs @@ -4,13 +4,15 @@ mod normalize; mod report; mod samples; -use crate::{error::Error, report::Report, samples::InterleavedSamples}; +use crate::{ + error::Error, + report::Report, + samples::{InterleavedSamples, PlanarSamples}, +}; use core::time::Duration; use ebur128_stream_rs as engine; use magnus::{ - Integer, RArray, Ruby, Symbol, TryConvert, Value, - error::IntoError, - function, method, + Integer, RArray, Ruby, Symbol, TryConvert, Value, function, method, prelude::*, scan_args::{get_kwargs, scan_args}, }; @@ -104,26 +106,8 @@ impl Analyzer { Ok(()) } - fn push_planar(ruby: &Ruby, rb_self: &Self, samples: Value) -> Result<(), Error> { - let samples: Vec> = if let Some(channels) = RArray::from_value(samples) { - channels - .into_iter() - .map(|channel| { - let ch = RArray::from_value(channel).ok_or_else(|| { - Error::argument(format!("channel not Array: {channel}")).into_error(ruby) - })?; - ch.into_iter() - .map(f32::try_convert) - .collect::, magnus::Error>>() - }) - .collect::>, magnus::Error>>()? - } else { - return Err(Error::argument(format!( - "unsupported samples type: {samples}" - )))?; - }; - let samples: Vec<&[f32]> = samples.iter().map(|channel| &channel[..]).collect(); - + fn push_planar(rb_self: &Self, samples: Value) -> Result<(), Error> { + let samples = PlanarSamples::try_convert(samples)?; let mut analyzer = rb_self .analyzer .try_borrow_mut() @@ -131,7 +115,7 @@ impl Analyzer { let analyzer = analyzer .as_mut() .ok_or_else(|| Error::runtime("analyzer not initialized"))?; - analyzer.push_planar(&samples)?; + analyzer.push_planar(&samples.channel_slices())?; Ok(()) } diff --git a/bindings/ruby/ext/ebur128_stream/src/memory_view.rs b/bindings/ruby/ext/ebur128_stream/src/memory_view.rs index 45718fb..2f52b76 100644 --- a/bindings/ruby/ext/ebur128_stream/src/memory_view.rs +++ b/bindings/ruby/ext/ebur128_stream/src/memory_view.rs @@ -143,6 +143,10 @@ impl MemoryView { unsafe { std::mem::transmute::(self.inner.obj) } } + pub fn ndim(&self) -> i64 { + self.inner.ndim + } + pub fn byte_size(&self) -> Result { usize::try_from(self.inner.byte_size).map_err(|err| { let obj = self.obj(); diff --git a/bindings/ruby/ext/ebur128_stream/src/samples.rs b/bindings/ruby/ext/ebur128_stream/src/samples.rs index 9d8e9e2..258110a 100644 --- a/bindings/ruby/ext/ebur128_stream/src/samples.rs +++ b/bindings/ruby/ext/ebur128_stream/src/samples.rs @@ -74,3 +74,55 @@ impl InterleavedSamples { None } } + +pub(crate) enum PlanarSamples { + Array { samples: Vec> }, + MemoryView { view: MemoryView }, +} + +impl TryConvert for PlanarSamples { + fn try_convert(val: Value) -> Result { + if let Some(memory_view) = Self::consume_memory_view(val) { + Ok(memory_view) + } else if let Some(obj) = RArray::from_value(val) { + Ok(Self::Array { + samples: obj.to_vec()?, + }) + } else { + Err(Error::argument(format!("unsupported samples type: {val}")) + .into_error(&Ruby::get_with(val))) + } + } +} + +impl PlanarSamples { + pub fn channel_slices(&self) -> Vec<&[f32]> { + match self { + Self::Array { samples } => samples.iter().map(Vec::as_slice).collect(), + Self::MemoryView { view } => view.data().unwrap_or(&[]).to_vec(), + } + } + + fn consume_memory_view(val: Value) -> Option { + let view = MemoryView::get(val, Flags::writable().format().row_major()); + if let Ok(view) = view { + if view.ndim() == 2 + && let Some(format) = view.format() + && format == "f" + { + return Some(Self::MemoryView { view }); + } + } + let view = MemoryView::get(val, Flags::simple()); + if let Ok(view) = view { + if !view.is_readonly() + && view.ndim() == 2 + && view.format().is_some() + && view.format().unwrap() == "f" + { + return Some(Self::MemoryView { view }); + } + } + None + } +} From 500f425bc7f14759d7ebcf1c651ea1d2353a83f9 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Fri, 14 Aug 2026 02:16:17 +0900 Subject: [PATCH 065/122] chore(ruby): change method name --- bindings/ruby/ext/ebur128_stream/src/samples.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bindings/ruby/ext/ebur128_stream/src/samples.rs b/bindings/ruby/ext/ebur128_stream/src/samples.rs index 258110a..0dbbda9 100644 --- a/bindings/ruby/ext/ebur128_stream/src/samples.rs +++ b/bindings/ruby/ext/ebur128_stream/src/samples.rs @@ -15,7 +15,7 @@ pub(crate) enum InterleavedSamples { impl TryConvert for InterleavedSamples { fn try_convert(val: Value) -> Result { Ok( - if let Some(memory_view) = Self::try_consume_memory_view(val) { + if let Some(memory_view) = Self::consume_memory_view(val) { memory_view } else if let Some(obj) = RArray::from_value(val) { let samples = obj.to_vec::()?; @@ -55,7 +55,7 @@ impl InterleavedSamples { Ok(()) } - fn try_consume_memory_view(val: Value) -> Option { + fn consume_memory_view(val: Value) -> Option { let view = MemoryView::get(val, Flags::simple()); if let Ok(view) = view { if !view.is_readonly() && view.format().is_some() && view.format().unwrap() == "f" { From 4aff603615d840cb4fde969f176cd38a06916cfe Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Fri, 14 Aug 2026 02:17:47 +0900 Subject: [PATCH 066/122] chore(ruby): refactor InterleavedSamples::try_convert() --- .../ruby/ext/ebur128_stream/src/samples.rs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/bindings/ruby/ext/ebur128_stream/src/samples.rs b/bindings/ruby/ext/ebur128_stream/src/samples.rs index 0dbbda9..36486da 100644 --- a/bindings/ruby/ext/ebur128_stream/src/samples.rs +++ b/bindings/ruby/ext/ebur128_stream/src/samples.rs @@ -14,17 +14,17 @@ pub(crate) enum InterleavedSamples { impl TryConvert for InterleavedSamples { fn try_convert(val: Value) -> Result { - Ok( - if let Some(memory_view) = Self::consume_memory_view(val) { - memory_view - } else if let Some(obj) = RArray::from_value(val) { - let samples = obj.to_vec::()?; - Self::Array { obj, samples } - } else { - return Err(Error::argument(format!("unsupported samples type: {val}")) - .into_error(&Ruby::get_with(val))); - }, - ) + if let Some(memory_view) = Self::consume_memory_view(val) { + Ok(memory_view) + } else if let Some(obj) = RArray::from_value(val) { + Ok(Self::Array { + obj, + samples: obj.to_vec()?, + }) + } else { + Err(Error::argument(format!("unsupported samples type: {val}")) + .into_error(&Ruby::get_with(val))) + } } } From c32b4685540a83df74e32cf275641d2542880e18 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Fri, 14 Aug 2026 02:18:35 +0900 Subject: [PATCH 067/122] fix(ruby): try ideal MemoryView first --- bindings/ruby/ext/ebur128_stream/src/samples.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/bindings/ruby/ext/ebur128_stream/src/samples.rs b/bindings/ruby/ext/ebur128_stream/src/samples.rs index 36486da..b60ccdd 100644 --- a/bindings/ruby/ext/ebur128_stream/src/samples.rs +++ b/bindings/ruby/ext/ebur128_stream/src/samples.rs @@ -56,21 +56,21 @@ impl InterleavedSamples { } fn consume_memory_view(val: Value) -> Option { - let view = MemoryView::get(val, Flags::simple()); - if let Ok(view) = view { - if !view.is_readonly() && view.format().is_some() && view.format().unwrap() == "f" { - // TODO: Check format more strictly(size, other expression) - return Some(Self::MemoryView { view }); - } - } let view = MemoryView::get(val, Flags::writable().any_contiguous()); if let Ok(view) = view { if let Some(format) = view.format() { + // TODO: Check format more strictly(size, other expression) if format == "f" { return Some(Self::MemoryView { view }); } } } + let view = MemoryView::get(val, Flags::simple()); + if let Ok(view) = view { + if !view.is_readonly() && view.format().is_some() && view.format().unwrap() == "f" { + return Some(Self::MemoryView { view }); + } + } None } } From 3f09488aa939593bea5ce0b54280237155ada720 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Fri, 14 Aug 2026 10:08:05 +0900 Subject: [PATCH 068/122] feat(ruby): refine MemoryView --- bindings/ruby/ext/ebur128_stream/Cargo.toml | 2 +- .../ext/ebur128_stream/src/memory_view.rs | 176 +++++++++++++----- .../ruby/ext/ebur128_stream/src/samples.rs | 40 ++-- 3 files changed, 154 insertions(+), 64 deletions(-) diff --git a/bindings/ruby/ext/ebur128_stream/Cargo.toml b/bindings/ruby/ext/ebur128_stream/Cargo.toml index 51698f3..f61a746 100644 --- a/bindings/ruby/ext/ebur128_stream/Cargo.toml +++ b/bindings/ruby/ext/ebur128_stream/Cargo.toml @@ -9,7 +9,7 @@ publish = false crate-type = ["cdylib"] [dependencies] -magnus = { version = "0.8.2" } +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"] } diff --git a/bindings/ruby/ext/ebur128_stream/src/memory_view.rs b/bindings/ruby/ext/ebur128_stream/src/memory_view.rs index 2f52b76..b39e954 100644 --- a/bindings/ruby/ext/ebur128_stream/src/memory_view.rs +++ b/bindings/ruby/ext/ebur128_stream/src/memory_view.rs @@ -1,13 +1,14 @@ use magnus::{Error, Ruby, Value}; use rb_sys::{ - VALUE, rb_memory_view_get, rb_memory_view_release, rb_memory_view_t, + Qnil, VALUE, rb_memory_view_get, rb_memory_view_parse_item_format, rb_memory_view_release, + rb_memory_view_t, ruby_memory_view_flags::{ RUBY_MEMORY_VIEW_ANY_CONTIGUOUS, RUBY_MEMORY_VIEW_COLUMN_MAJOR, RUBY_MEMORY_VIEW_FORMAT, RUBY_MEMORY_VIEW_INDIRECT, RUBY_MEMORY_VIEW_MULTI_DIMENSIONAL, RUBY_MEMORY_VIEW_ROW_MAJOR, RUBY_MEMORY_VIEW_SIMPLE, RUBY_MEMORY_VIEW_STRIDES, RUBY_MEMORY_VIEW_WRITABLE, }, }; -use std::{ffi::CStr, mem::MaybeUninit, slice}; +use std::{ffi::CStr, mem::MaybeUninit, ptr, slice, marker::PhantomData}; pub struct Flags(i32); @@ -108,11 +109,12 @@ impl FlagsChainable for Flags { } } -pub struct MemoryView { +pub struct MemoryView { inner: rb_memory_view_t, + marker: PhantomData, } -impl Drop for MemoryView { +impl Drop for MemoryView { fn drop(&mut self) { // SAFETY: This guard is generated from successfully allocated rb_memory_view_t and drop() is called only once so it's not released more than once unsafe { @@ -121,13 +123,18 @@ impl Drop for MemoryView { } } -impl MemoryView { +impl MemoryView { pub fn get(obj: Value, flags: Flags) -> Result { let ruby = Ruby::get_with(obj); let obj = unsafe { std::mem::transmute::(obj) }; let mut view = MaybeUninit::uninit(); - let result = unsafe { rb_memory_view_get(obj, view.as_mut_ptr(), flags.into()) }; + + let mut result = false; + magnus::rb_sys::protect(|| { + result = unsafe { rb_memory_view_get(obj, view.as_mut_ptr(), flags.into()) }; + Qnil.into() + })?; if !result { return Err(Error::new( ruby.exception_runtime_error(), @@ -135,24 +142,83 @@ impl MemoryView { )); } let view = unsafe { view.assume_init() }; + let view = Self { inner: view, marker: PhantomData }; + + // Validation + let ndim = usize::try_from(view.inner.ndim) + .map_err(|_| Error::new(ruby.exception_arg_error(), "invalid ndim"))?; + usize::try_from(view.inner.byte_size) + .map_err(|_| Error::new(ruby.exception_arg_error(), "invalid byte_size"))?; + let item_size = usize::try_from(view.inner.item_size) + .map_err(|_| Error::new(ruby.exception_arg_error(), "invalid item_size"))?; + + let format = view.inner.format; + if !format.is_null() { + let format = unsafe { CStr::from_ptr(format) }; + let item_size_by_format = Self::validate_format(&ruby, format)?; + if item_size_by_format != item_size { + Err(Error::new( + ruby.exception_arg_error(), + "item_size and item size calculated by format not match", + ))?; + } + } + + if view.inner.shape.is_null() { + if ndim > 1 { + Err(Error::new( + ruby.exception_arg_error(), + "ndim > 1 but shape is NULL", + ))?; + } + } else { + // SAFETY: rb_memory_view_t.shape is *ssize_t + let shape = unsafe { slice::from_raw_parts(view.inner.shape, ndim) }; + if !view.inner.shape.cast::().is_aligned() { + Err(Error::new( + ruby.exception_arg_error(), + "shape not aligned for usize", + ))?; + } + for &dim in shape { + usize::try_from(dim).map_err(|_| { + Error::new( + ruby.exception_arg_error(), + format!("dimension {dim} of shape invalid"), + ) + })?; + } + } - Ok(Self { inner: view }) + let data = view.inner.data; + if data.is_null() { + Err(Error::new(ruby.exception_runtime_error(), "data is NULL"))?; + } + let ptr = data.cast::(); + if !ptr.is_aligned() { + Err(Error::new( + ruby.exception_runtime_error(), + "data not aligned", + ))?; + } + + Ok(view) } - fn obj(&self) -> Value { - unsafe { std::mem::transmute::(self.inner.obj) } + pub fn ndim(&self) -> usize { + usize::try_from(self.inner.ndim).expect("ndim validated in get()") } - pub fn ndim(&self) -> i64 { - self.inner.ndim + pub fn byte_size(&self) -> usize { + usize::try_from(self.inner.byte_size).expect("byte_size validated in get()") } - pub fn byte_size(&self) -> Result { - usize::try_from(self.inner.byte_size).map_err(|err| { - let obj = self.obj(); - let ruby = Ruby::get_with(obj); - Error::new(ruby.exception_runtime_error(), format!("{err}")) - }) + pub fn shape(&self) -> Option<&[usize]> { + if self.inner.shape.is_null() { + return None; + } + // SAFETY: validated in get() + Some(unsafe { slice::from_raw_parts(self.inner.shape.cast::(), self.ndim()) }) } pub fn is_readonly(&self) -> bool { @@ -163,49 +229,61 @@ impl MemoryView { if self.inner.format.is_null() { None } else { - unsafe { CStr::from_ptr(self.inner.format).to_str() }.ok() + // SAFETY: format is valid because parse_item_format() is called in get() + Some( + unsafe { CStr::from_ptr(self.inner.format) } + .to_str() + .unwrap(), + ) } } - pub fn data(&self) -> Result<&[T], Error> { - let n_items = self.byte_size()? / size_of::(); + pub fn data(&self) -> &[T] { + let n_items = self.byte_size() / size_of::(); let data = self.inner.data; - if data.is_null() { - let ruby = Ruby::get_with(self.obj()); - return Err(Error::new(ruby.exception_runtime_error(), "data is NULL")); - } let ptr = data.cast::(); - if !ptr.is_aligned() { - let ruby = Ruby::get_with(self.obj()); - return Err(Error::new( - ruby.exception_runtime_error(), - "data not aligned", - )); - } - Ok(unsafe { slice::from_raw_parts(ptr, n_items) }) + unsafe { slice::from_raw_parts(ptr, n_items) } } - pub fn data_as_mut(&mut self) -> Result<&mut [T], Error> { - if self.is_readonly() { - let ruby = Ruby::get_with(self.obj()); - return Err(Error::new(ruby.exception_runtime_error(), "not mutable")); - } - let n_items = self.byte_size()? / size_of::(); + pub fn data_as_mut(&mut self) -> &mut [T] { + let n_items = self.byte_size() / size_of::(); let data = self.inner.data; - if data.is_null() { - let ruby = Ruby::get_with(self.obj()); - return Err(Error::new(ruby.exception_runtime_error(), "data is NULL")); - } let ptr = data.cast::(); - if !ptr.is_aligned() { - let ruby = Ruby::get_with(self.obj()); - return Err(Error::new( + + unsafe { slice::from_raw_parts_mut(ptr, n_items) } + } + + // Just for validation and retrieving item size + // TODO: Implement parse_item_format() properly + fn validate_format(ruby: &Ruby, format: &CStr) -> Result { + let mut item_size = -1; + + magnus::rb_sys::protect(|| { + item_size = unsafe { + rb_memory_view_parse_item_format( + format.as_ptr(), + ptr::null_mut(), + ptr::null_mut(), + ptr::null_mut(), + ) + }; + Qnil.into() + })?; + + if item_size < 0 { + Err(Error::new( ruby.exception_runtime_error(), - "data not aligned", - )); + "failed to parse format", + )) + } else { + let item_size = usize::try_from(item_size).map_err(|_| { + Error::new( + ruby.exception_runtime_error(), + "format parse returned invalid item_size", + ) + })?; + Ok(item_size) } - - Ok(unsafe { slice::from_raw_parts_mut(ptr, n_items) }) } } diff --git a/bindings/ruby/ext/ebur128_stream/src/samples.rs b/bindings/ruby/ext/ebur128_stream/src/samples.rs index b60ccdd..77d41a4 100644 --- a/bindings/ruby/ext/ebur128_stream/src/samples.rs +++ b/bindings/ruby/ext/ebur128_stream/src/samples.rs @@ -3,13 +3,10 @@ use crate::{ memory_view::{Flags, FlagsChainable, MemoryView}, }; use magnus::{RArray, Ruby, TryConvert, Value, error::IntoError}; -use rb_sys::ruby_memory_view_flags::{ - RUBY_MEMORY_VIEW_ANY_CONTIGUOUS, RUBY_MEMORY_VIEW_SIMPLE, RUBY_MEMORY_VIEW_WRITABLE, -}; pub(crate) enum InterleavedSamples { Array { obj: RArray, samples: Vec }, - MemoryView { view: MemoryView }, + MemoryView { view: MemoryView }, } impl TryConvert for InterleavedSamples { @@ -32,14 +29,14 @@ impl InterleavedSamples { pub(crate) fn as_slice(&self) -> &[f32] { match self { Self::Array { obj: _, samples } => samples, - Self::MemoryView { view } => view.data().unwrap_or(&[]), + Self::MemoryView { view } => view.data(), } } pub(crate) fn as_mut_slice(&mut self) -> &mut [f32] { match self { Self::Array { obj: _, samples } => samples, - Self::MemoryView { view } => view.data_as_mut().unwrap_or(&mut []), + Self::MemoryView { view } => view.data_as_mut(), } } @@ -56,18 +53,24 @@ impl InterleavedSamples { } fn consume_memory_view(val: Value) -> Option { - let view = MemoryView::get(val, Flags::writable().any_contiguous()); + let view = MemoryView::::get(val, Flags::writable().format().any_contiguous()); if let Ok(view) = view { - if let Some(format) = view.format() { + if view.ndim() == 1 + && let Some(format) = view.format() + { // TODO: Check format more strictly(size, other expression) if format == "f" { return Some(Self::MemoryView { view }); } } } - let view = MemoryView::get(val, Flags::simple()); + let view = MemoryView::::get(val, Flags::simple()); if let Ok(view) = view { - if !view.is_readonly() && view.format().is_some() && view.format().unwrap() == "f" { + if !view.is_readonly() + && view.ndim() == 1 + && view.format().is_some() + && view.format().unwrap() == "f" + { return Some(Self::MemoryView { view }); } } @@ -77,7 +80,7 @@ impl InterleavedSamples { pub(crate) enum PlanarSamples { Array { samples: Vec> }, - MemoryView { view: MemoryView }, + MemoryView { view: MemoryView }, } impl TryConvert for PlanarSamples { @@ -99,12 +102,21 @@ impl PlanarSamples { pub fn channel_slices(&self) -> Vec<&[f32]> { match self { Self::Array { samples } => samples.iter().map(Vec::as_slice).collect(), - Self::MemoryView { view } => view.data().unwrap_or(&[]).to_vec(), + Self::MemoryView { view } => { + let shape = view.shape().expect("ndim > 1 is checked when calling "); + let n_channels = shape[0]; + let channel_len = shape[1]; + if channel_len == 0 { + (0..n_channels).map(|_| &view.data()[..0]).collect() + } else { + view.data().chunks_exact(channel_len).collect() + } + } } } fn consume_memory_view(val: Value) -> Option { - let view = MemoryView::get(val, Flags::writable().format().row_major()); + let view = MemoryView::::get(val, Flags::writable().format().row_major()); if let Ok(view) = view { if view.ndim() == 2 && let Some(format) = view.format() @@ -113,7 +125,7 @@ impl PlanarSamples { return Some(Self::MemoryView { view }); } } - let view = MemoryView::get(val, Flags::simple()); + let view = MemoryView::::get(val, Flags::simple()); if let Ok(view) = view { if !view.is_readonly() && view.ndim() == 2 From 019925cb2b6eae1487549bf3f4f3dfb639b131d5 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Sat, 15 Aug 2026 07:13:19 +0900 Subject: [PATCH 069/122] chore(ruby): Remove module qualification --- bindings/ruby/test/test_ebur128_stream.rb | 24 ++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/bindings/ruby/test/test_ebur128_stream.rb b/bindings/ruby/test/test_ebur128_stream.rb index 054ad05..8250b0a 100644 --- a/bindings/ruby/test/test_ebur128_stream.rb +++ b/bindings/ruby/test/test_ebur128_stream.rb @@ -3,6 +3,8 @@ require_relative "helper" class EBUR128StreamTest < Test::Unit::TestCase + include EBUR128Stream + test "VERSION" do assert do ::EBUR128Stream.const_defined?(:VERSION) @@ -11,24 +13,24 @@ class EBUR128StreamTest < Test::Unit::TestCase test "new" do assert_raise ArgumentError do - EBUR128Stream::Analyzer.new + Analyzer.new end - assert_instance_of EBUR128Stream::Analyzer, EBUR128Stream::Analyzer.new(channels: [:left]) + assert_instance_of Analyzer, Analyzer.new(channels: [:left]) assert_raise ArgumentError do - EBUR128Stream::Analyzer.new(channels: [:left, :nothing]) + Analyzer.new(channels: [:left, :nothing]) end - assert_instance_of EBUR128Stream::Analyzer, EBUR128Stream::Analyzer.new(channels: [:center], sample_rate: 48_000) + assert_instance_of Analyzer, Analyzer.new(channels: [:center], sample_rate: 48_000) assert_raise RuntimeError do - EBUR128Stream::Analyzer.new(channels: [:center], modes: []) + Analyzer.new(channels: [:center], modes: []) end - assert_instance_of EBUR128Stream::Analyzer, EBUR128Stream::Analyzer.new(channels: [:center], modes: [:integrated, :true_peak]) + assert_instance_of Analyzer, Analyzer.new(channels: [:center], modes: [:integrated, :true_peak]) assert_raise ArgumentError do - EBUR128Stream::Analyzer.new(channels: [:center], modes: [:unknown]) + Analyzer.new(channels: [:center], modes: [:unknown]) end end test "push_interleaved" do - analyzer = EBUR128Stream::Analyzer.new(channels: [:left, :right], modes: [:all]) + analyzer = Analyzer.new(channels: [:left, :right], modes: [:all]) assert_nothing_raised do analyzer.push_interleaved [1.0, 1.0, 2.0, 2.0, 3.0, 3.0] end @@ -38,7 +40,7 @@ class EBUR128StreamTest < Test::Unit::TestCase end test "push_planar" do - analyzer = EBUR128Stream::Analyzer.new(channels: [:left, :right], modes: [:all]) + analyzer = Analyzer.new(channels: [:left, :right], modes: [:all]) assert_nothing_raised do analyzer.push_planar [[1.0, 2.0, 3.0], [1.0, 2.0, 3.0]] analyzer.push_planar [[1.0, 2.0, 3.0], [1.0, 2.0, 3.0]] @@ -49,13 +51,13 @@ class EBUR128StreamTest < Test::Unit::TestCase end test "finalize" do - analyzer = EBUR128Stream::Analyzer.new(channels: [:left, :right], modes: [:all]) + analyzer = Analyzer.new(channels: [:left, :right], modes: [:all]) analyzer.push_interleaved [1.0, 1.0, 2.0, 2.0, 3.0, 3.0] report = nil assert_nothing_raised do report = analyzer.finalize end - assert_instance_of EBUR128Stream::Report, report + assert_instance_of Report, report assert_raise_with_message RuntimeError, /finalized/ do analyzer.finalize end From 4b321775ebb002cce31bef5c0b25bf889ca777d3 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Sat, 15 Aug 2026 08:12:42 +0900 Subject: [PATCH 070/122] fix(ruby): allow readonly MemoryView for InterleavedSamples --- .../ext/ebur128_stream/src/memory_view.rs | 8 ++++-- .../ruby/ext/ebur128_stream/src/normalize.rs | 3 +++ .../ruby/ext/ebur128_stream/src/samples.rs | 25 +++++++++++++++---- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/bindings/ruby/ext/ebur128_stream/src/memory_view.rs b/bindings/ruby/ext/ebur128_stream/src/memory_view.rs index b39e954..9c38845 100644 --- a/bindings/ruby/ext/ebur128_stream/src/memory_view.rs +++ b/bindings/ruby/ext/ebur128_stream/src/memory_view.rs @@ -8,7 +8,7 @@ use rb_sys::{ RUBY_MEMORY_VIEW_SIMPLE, RUBY_MEMORY_VIEW_STRIDES, RUBY_MEMORY_VIEW_WRITABLE, }, }; -use std::{ffi::CStr, mem::MaybeUninit, ptr, slice, marker::PhantomData}; +use std::{ffi::CStr, marker::PhantomData, mem::MaybeUninit, ptr, slice}; pub struct Flags(i32); @@ -141,8 +141,12 @@ impl MemoryView { "MemoryView not got", )); } + let view = unsafe { view.assume_init() }; - let view = Self { inner: view, marker: PhantomData }; + let view = Self { + inner: view, + marker: PhantomData, + }; // Validation let ndim = usize::try_from(view.inner.ndim) diff --git a/bindings/ruby/ext/ebur128_stream/src/normalize.rs b/bindings/ruby/ext/ebur128_stream/src/normalize.rs index c9dc03e..b34a8f7 100644 --- a/bindings/ruby/ext/ebur128_stream/src/normalize.rs +++ b/bindings/ruby/ext/ebur128_stream/src/normalize.rs @@ -38,6 +38,9 @@ impl Normalizer { fn normalize_in_place(rb_self: &Self, samples: Value) -> Result { let mut frames = InterleavedSamples::try_convert(samples)?; + if !frames.is_writable() { + return Err(Error::argument("samples not writable")); + } let mut normalizer = engine::normalize::Normalizer::new(rb_self.sample_rate, &rb_self.channels); if let Some(target_lufs) = rb_self.target_lufs { diff --git a/bindings/ruby/ext/ebur128_stream/src/samples.rs b/bindings/ruby/ext/ebur128_stream/src/samples.rs index 77d41a4..26b9f07 100644 --- a/bindings/ruby/ext/ebur128_stream/src/samples.rs +++ b/bindings/ruby/ext/ebur128_stream/src/samples.rs @@ -26,6 +26,15 @@ impl TryConvert for InterleavedSamples { } impl InterleavedSamples { + // Instead of this method, should add Writable/ReadableInterleavedSamples, + // or try_concert_mut()? + pub(crate) fn is_writable(&self) -> bool { + match self { + Self::Array { .. } => true, + Self::MemoryView { view } => !view.is_readonly(), + } + } + pub(crate) fn as_slice(&self) -> &[f32] { match self { Self::Array { obj: _, samples } => samples, @@ -64,16 +73,22 @@ impl InterleavedSamples { } } } - let view = MemoryView::::get(val, Flags::simple()); + let view = MemoryView::::get(val, Flags::format().any_contiguous()); if let Ok(view) = view { - if !view.is_readonly() - && view.ndim() == 1 - && view.format().is_some() - && view.format().unwrap() == "f" + if view.ndim() == 1 + && let Some(format) = view.format() + && format == "f" { return Some(Self::MemoryView { view }); } } + let view = MemoryView::::get(val, Flags::simple()); + if let Ok(view) = view { + let format = view.format(); + if view.ndim() == 1 && format.is_some() && format.unwrap() == "f" { + return Some(Self::MemoryView { view }); + } + } None } } From dc208dbdc274a03b23289074cf04a75def27eced Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Sat, 15 Aug 2026 07:25:05 +0900 Subject: [PATCH 071/122] feat(ruby): add Numo::NArray Alternative to dev dependency --- bindings/ruby/ebur128_stream.gemspec | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bindings/ruby/ebur128_stream.gemspec b/bindings/ruby/ebur128_stream.gemspec index d361da5..32a1d7d 100644 --- a/bindings/ruby/ebur128_stream.gemspec +++ b/bindings/ruby/ebur128_stream.gemspec @@ -46,6 +46,8 @@ Gem::Specification.new do |spec| 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/ From 89e97e613f8611b6918aa867a6e9f42ea3efcc07 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Sat, 15 Aug 2026 08:50:32 +0900 Subject: [PATCH 072/122] test(ruby): add test for MemoryView --- bindings/ruby/test/helper.rb | 2 ++ bindings/ruby/test/test_ebur128_stream.rb | 32 +++++++++++++++++++++++ bindings/ruby/test/test_normalizer.rb | 20 ++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/bindings/ruby/test/helper.rb b/bindings/ruby/test/helper.rb index 2e503cc..ba4683a 100644 --- a/bindings/ruby/test/helper.rb +++ b/bindings/ruby/test/helper.rb @@ -4,3 +4,5 @@ require "ebur128_stream" require "test-unit" +require "numo/narray/alt" +require "ndav/numo/narray" diff --git a/bindings/ruby/test/test_ebur128_stream.rb b/bindings/ruby/test/test_ebur128_stream.rb index 8250b0a..071859a 100644 --- a/bindings/ruby/test/test_ebur128_stream.rb +++ b/bindings/ruby/test/test_ebur128_stream.rb @@ -39,6 +39,27 @@ class EBUR128StreamTest < Test::Unit::TestCase end end + test "push_interleaved MemoryView" do + analyzer = Analyzer.new(channels: [:left, :right], modes: [:all]) + + # Assigning an narray to a variable causes segmentation fault: + # + # > ruby(354,0x1f9285d80) malloc: *** error for object 0x7c7aef0ba6980052: pointer being freed was not allocated + # > ruby(354,0x1f9285d80) malloc: *** set a breakpoint in malloc_error_break to debug + # + # valid_data = Numo::SFloat[1.0, 1.0, 2.0, 2.0, 3.0, 3.0] + # assert_nothing_raised do + # analyzer.push_interleaved valid_data + # end + + assert_nothing_raised do + analyzer.push_interleaved Numo::SFloat[1.0, 1.0, 2.0, 2.0, 3.0, 3.0] + end + assert_raise ArgumentError do + analyzer.push_interleaved Numo::SFloat[1.0, 1.0, 2.0] + end + end + test "push_planar" do analyzer = Analyzer.new(channels: [:left, :right], modes: [:all]) assert_nothing_raised do @@ -50,6 +71,17 @@ class EBUR128StreamTest < Test::Unit::TestCase end end + test "push_planar MemoryView" do + analyzer = Analyzer.new(channels: [:left, :right], modes: [:all]) + assert_nothing_raised do + analyzer.push_planar Numo::SFloat[[1.0, 2.0, 3.0], [1.0, 2.0, 3.0]] + analyzer.push_planar Numo::SFloat[[1.0, 2.0, 3.0], [1.0, 2.0, 3.0]] + end + assert_raise ArgumentError do + analyzer.push_planar Numo::SFloat[[1.0, 2.0, 3.0], [1.0, 2.0, 3.0], [1.0, 2.0, 3.0]] + end + end + test "finalize" do analyzer = Analyzer.new(channels: [:left, :right], modes: [:all]) analyzer.push_interleaved [1.0, 1.0, 2.0, 2.0, 3.0, 3.0] diff --git a/bindings/ruby/test/test_normalizer.rb b/bindings/ruby/test/test_normalizer.rb index 032e810..2cb410d 100644 --- a/bindings/ruby/test/test_normalizer.rb +++ b/bindings/ruby/test/test_normalizer.rb @@ -21,4 +21,24 @@ def test_normalize_in_place samples != before end end + + def test_normalize_in_place_memory_view + sample_rate = 48_000 + normalizer = Normalizer.new(channels: [:left, :right], sample_rate:) + frame = 2 * Math::PI / (sample_rate / 4) + samples = sample_rate.times.flat_map do |n| + value = Math.cos(frame * n) * 10 + end + samples_mv = Numo::SFloat.cast(samples) + normalizer.normalize_in_place(samples_mv) + + assert do + samples_mv.to_a != samples + end + + normalizer.normalize_in_place(samples) + assert do + samples_mv == samples + end + end end From 96cfb4d42c1af4cc7905cfea2efe6092fcf696d1 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Sat, 15 Aug 2026 09:15:20 +0900 Subject: [PATCH 073/122] feat(ruby): wrap rb_memory_view_release by protect --- bindings/ruby/ext/ebur128_stream/src/memory_view.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/bindings/ruby/ext/ebur128_stream/src/memory_view.rs b/bindings/ruby/ext/ebur128_stream/src/memory_view.rs index 9c38845..395ba3f 100644 --- a/bindings/ruby/ext/ebur128_stream/src/memory_view.rs +++ b/bindings/ruby/ext/ebur128_stream/src/memory_view.rs @@ -116,10 +116,13 @@ pub struct MemoryView { impl Drop for MemoryView { fn drop(&mut self) { - // SAFETY: This guard is generated from successfully allocated rb_memory_view_t and drop() is called only once so it's not released more than once - unsafe { - rb_memory_view_release(&mut self.inner); - } + let _ = magnus::rb_sys::protect(|| { + // SAFETY: This guard is generated from successfully allocated rb_memory_view_t and drop() is called only once so it's not released more than once + unsafe { + rb_memory_view_release(&mut self.inner); + } + Qnil.into() + }); } } From 28c89248e299abd4142244a0eaf0ebd885964df3 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Sat, 15 Aug 2026 09:25:28 +0900 Subject: [PATCH 074/122] docs(ruby): add comment on SEGV --- bindings/ruby/ext/ebur128_stream/src/memory_view.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/bindings/ruby/ext/ebur128_stream/src/memory_view.rs b/bindings/ruby/ext/ebur128_stream/src/memory_view.rs index 395ba3f..00715d9 100644 --- a/bindings/ruby/ext/ebur128_stream/src/memory_view.rs +++ b/bindings/ruby/ext/ebur128_stream/src/memory_view.rs @@ -115,6 +115,7 @@ pub struct MemoryView { } impl Drop for MemoryView { + // Causes segmentation fault in some cases. Needs the investigation. fn drop(&mut self) { let _ = magnus::rb_sys::protect(|| { // SAFETY: This guard is generated from successfully allocated rb_memory_view_t and drop() is called only once so it's not released more than once From ebb023fb0d68e00c9086b94892502d72740b207f Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Sat, 15 Aug 2026 13:48:44 +0900 Subject: [PATCH 075/122] feat(ruby): enable simd feature --- bindings/ruby/ext/ebur128_stream/Cargo.lock | 26 +++++++++++++++++++++ bindings/ruby/ext/ebur128_stream/Cargo.toml | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/bindings/ruby/ext/ebur128_stream/Cargo.lock b/bindings/ruby/ext/ebur128_stream/Cargo.lock index db64f80..52642ff 100644 --- a/bindings/ruby/ext/ebur128_stream/Cargo.lock +++ b/bindings/ruby/ext/ebur128_stream/Cargo.lock @@ -35,6 +35,12 @@ 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" @@ -80,6 +86,7 @@ checksum = "66e026ac32fee8397cdbaafee0030443af608d6c4832e590905b16c9450b7536" dependencies = [ "bitflags", "libm", + "wide", ] [[package]] @@ -280,6 +287,15 @@ 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" @@ -315,6 +331,16 @@ 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" diff --git a/bindings/ruby/ext/ebur128_stream/Cargo.toml b/bindings/ruby/ext/ebur128_stream/Cargo.toml index f61a746..cb776cd 100644 --- a/bindings/ruby/ext/ebur128_stream/Cargo.toml +++ b/bindings/ruby/ext/ebur128_stream/Cargo.toml @@ -11,7 +11,7 @@ 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"] } +ebur128-stream-rs = { package = "ebur128-stream", version = "0.2.0", features= ["normalize", "simd"] } [build-dependencies] rb-sys-env = "0.2.2" From 6bbeaa6ee8c685b62d33e7c480300be50edb1a28 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Sat, 15 Aug 2026 14:08:12 +0900 Subject: [PATCH 076/122] refactor(ruby): extractd analyzer module --- .../ruby/ext/ebur128_stream/src/analyzer.rs | 112 ++++++++++++++++++ bindings/ruby/ext/ebur128_stream/src/lib.rs | 109 +---------------- 2 files changed, 115 insertions(+), 106 deletions(-) create mode 100644 bindings/ruby/ext/ebur128_stream/src/analyzer.rs 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..18dc55b --- /dev/null +++ b/bindings/ruby/ext/ebur128_stream/src/analyzer.rs @@ -0,0 +1,112 @@ +use crate::{Error, InterleavedSamples, PlanarSamples, Report, parse_channels_arg}; +use ebur128_stream_rs as engine; +use magnus::{ + Integer, RArray, RModule, Ruby, Symbol, TryConvert, Value, function, method, + prelude::*, + scan_args::{get_kwargs, scan_args}, +}; +use std::{cell::RefCell, time::Duration}; + +#[magnus::wrap(class = "EBUR128Stream::Analyzer")] +struct Analyzer { + analyzer: RefCell>, +} + +impl Analyzer { + fn new(args: &[Value]) -> Result { + let args = scan_args::<(), (), (), (), _, ()>(args)?; + let kws = get_kwargs::<_, (RArray,), (Option, Option, Option), ()>( + args.keywords, + &["channels"], + &["sample_rate", "modes", "expected_duration"], + )?; + let (channels,) = kws.required; + let (sample_rate, modes, expected_duration) = kws.optional; + + let channels = parse_channels_arg(channels)?; + let mut builder = engine::AnalyzerBuilder::new().channels(&channels); + + if let Some(sample_rate) = sample_rate { + builder = builder.sample_rate(sample_rate.to_u32()?); + } + + if let Some(values) = modes { + use engine::Mode; + + let mut modes = Mode::empty(); + for value in values.into_iter() { + let mode = Symbol::try_convert(value)?; + modes |= match mode.name()?.as_ref() { + "integrated" => Mode::Integrated, + "momentary" => Mode::Momentary, + "short_term" => Mode::ShortTerm, + "true_peak" => Mode::TruePeak, + "lra" => Mode::Lra, + "all" => Mode::All, + _ => { + return Err(Error::argument(format!("unknown mode: {mode}")))?; + } + } + } + builder = builder.modes(modes); + } + if let Some(expected_duration) = expected_duration { + builder = builder.expected_duration(Duration::from_secs(expected_duration.to_u64()?)); + } + + let analyzer = builder.build().map_err(Error::runtime)?; + + Ok(Self { + analyzer: RefCell::new(Some(analyzer)), + }) + } + + fn push_interleaved(rb_self: &Self, samples: Value) -> Result<(), Error> { + let samples = InterleavedSamples::try_convert(samples)?; + + let mut analyzer = rb_self.analyzer.try_borrow_mut().map_err(Error::runtime)?; + let analyzer = analyzer + .as_mut() + .ok_or_else(|| Error::runtime("analyzer not initialized"))?; + analyzer.push_interleaved(samples.as_slice())?; + + Ok(()) + } + + fn push_planar(rb_self: &Self, samples: Value) -> Result<(), Error> { + let samples = PlanarSamples::try_convert(samples)?; + let mut analyzer = rb_self + .analyzer + .try_borrow_mut() + .map_err(|_| Error::runtime("analyzer already in use"))?; + let analyzer = analyzer + .as_mut() + .ok_or_else(|| Error::runtime("analyzer not initialized"))?; + analyzer.push_planar(&samples.channel_slices())?; + + Ok(()) + } + + fn finalize(rb_self: &Self) -> Result { + let mut analyzer = rb_self + .analyzer + .try_borrow_mut() + .map_err(|_| Error::runtime("analyzer already in use"))?; + let analyzer = analyzer + .take() + .ok_or_else(|| Error::runtime("analyzer already finalized"))?; + let report = analyzer.finalize(); + + Ok(Report { report }) + } +} + +pub(crate) fn init(ruby: &Ruby, module: &RModule) -> Result<(), Error> { + let analyzer = module.define_class("Analyzer", ruby.class_object())?; + analyzer.define_singleton_method("new", function!(Analyzer::new, -1))?; + analyzer.define_method("push_interleaved", method!(Analyzer::push_interleaved, 1))?; + analyzer.define_method("push_planar", method!(Analyzer::push_planar, 1))?; + analyzer.define_method("finalize", method!(Analyzer::finalize, 0))?; + + Ok(()) +} diff --git a/bindings/ruby/ext/ebur128_stream/src/lib.rs b/bindings/ruby/ext/ebur128_stream/src/lib.rs index 0ada55c..bc1f810 100644 --- a/bindings/ruby/ext/ebur128_stream/src/lib.rs +++ b/bindings/ruby/ext/ebur128_stream/src/lib.rs @@ -1,3 +1,4 @@ +mod analyzer; mod error; mod memory_view; mod normalize; @@ -9,14 +10,8 @@ use crate::{ report::Report, samples::{InterleavedSamples, PlanarSamples}, }; -use core::time::Duration; use ebur128_stream_rs as engine; -use magnus::{ - Integer, RArray, Ruby, Symbol, TryConvert, Value, function, method, - prelude::*, - scan_args::{get_kwargs, scan_args}, -}; -use std::cell::RefCell; +use magnus::{RArray, Ruby, Symbol, TryConvert}; // can make channels a slice? pub(crate) fn parse_channels_arg(channels: RArray) -> Result, Error> { @@ -40,109 +35,11 @@ pub(crate) fn parse_channels_arg(channels: RArray) -> Result>, -} - -impl Analyzer { - fn new(args: &[Value]) -> Result { - let args = scan_args::<(), (), (), (), _, ()>(args)?; - let kws = get_kwargs::<_, (RArray,), (Option, Option, Option), ()>( - args.keywords, - &["channels"], - &["sample_rate", "modes", "expected_duration"], - )?; - let (channels,) = kws.required; - let (sample_rate, modes, expected_duration) = kws.optional; - - let channels = parse_channels_arg(channels)?; - let mut builder = engine::AnalyzerBuilder::new().channels(&channels); - - if let Some(sample_rate) = sample_rate { - builder = builder.sample_rate(sample_rate.to_u32()?); - } - - if let Some(values) = modes { - use engine::Mode; - - let mut modes = Mode::empty(); - for value in values.into_iter() { - let mode = Symbol::try_convert(value)?; - modes |= match mode.name()?.as_ref() { - "integrated" => Mode::Integrated, - "momentary" => Mode::Momentary, - "short_term" => Mode::ShortTerm, - "true_peak" => Mode::TruePeak, - "lra" => Mode::Lra, - "all" => Mode::All, - _ => { - return Err(Error::argument(format!("unknown mode: {mode}")))?; - } - } - } - builder = builder.modes(modes); - } - if let Some(expected_duration) = expected_duration { - builder = builder.expected_duration(Duration::from_secs(expected_duration.to_u64()?)); - } - - let analyzer = builder.build().map_err(Error::runtime)?; - - Ok(Self { - analyzer: RefCell::new(Some(analyzer)), - }) - } - - fn push_interleaved(rb_self: &Self, samples: Value) -> Result<(), Error> { - let samples = InterleavedSamples::try_convert(samples)?; - - let mut analyzer = rb_self.analyzer.try_borrow_mut().map_err(Error::runtime)?; - let analyzer = analyzer - .as_mut() - .ok_or_else(|| Error::runtime("analyzer not initialized"))?; - analyzer.push_interleaved(samples.as_slice())?; - - Ok(()) - } - - fn push_planar(rb_self: &Self, samples: Value) -> Result<(), Error> { - let samples = PlanarSamples::try_convert(samples)?; - let mut analyzer = rb_self - .analyzer - .try_borrow_mut() - .map_err(|_| Error::runtime("analyzer already in use"))?; - let analyzer = analyzer - .as_mut() - .ok_or_else(|| Error::runtime("analyzer not initialized"))?; - analyzer.push_planar(&samples.channel_slices())?; - - Ok(()) - } - - fn finalize(rb_self: &Self) -> Result { - let mut analyzer = rb_self - .analyzer - .try_borrow_mut() - .map_err(|_| Error::runtime("analyzer already in use"))?; - let analyzer = analyzer - .take() - .ok_or_else(|| Error::runtime("analyzer already finalized"))?; - let report = analyzer.finalize(); - - Ok(Report { report }) - } -} - #[magnus::init] fn init(ruby: &Ruby) -> Result<(), Error> { let ebur128_stream = ruby.define_module("EBUR128Stream")?; - let analyzer = ebur128_stream.define_class("Analyzer", ruby.class_object())?; - analyzer.define_singleton_method("new", function!(Analyzer::new, -1))?; - analyzer.define_method("push_interleaved", method!(Analyzer::push_interleaved, 1))?; - analyzer.define_method("push_planar", method!(Analyzer::push_planar, 1))?; - analyzer.define_method("finalize", method!(Analyzer::finalize, 0))?; + analyzer::init(ruby, &ebur128_stream)?; report::init(ruby, &ebur128_stream)?; normalize::init(ruby, &ebur128_stream)?; From 87a6ff276f229292f8c0b3afd217f28299669bd4 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Sat, 15 Aug 2026 14:13:10 +0900 Subject: [PATCH 077/122] refactor(ruby): remove manual try_convert --- bindings/ruby/ext/ebur128_stream/src/analyzer.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bindings/ruby/ext/ebur128_stream/src/analyzer.rs b/bindings/ruby/ext/ebur128_stream/src/analyzer.rs index 18dc55b..11d7262 100644 --- a/bindings/ruby/ext/ebur128_stream/src/analyzer.rs +++ b/bindings/ruby/ext/ebur128_stream/src/analyzer.rs @@ -61,6 +61,7 @@ impl Analyzer { }) } + // Segmentation fault occurs if samples: InterleavedSamples in arguments fn push_interleaved(rb_self: &Self, samples: Value) -> Result<(), Error> { let samples = InterleavedSamples::try_convert(samples)?; @@ -73,8 +74,7 @@ impl Analyzer { Ok(()) } - fn push_planar(rb_self: &Self, samples: Value) -> Result<(), Error> { - let samples = PlanarSamples::try_convert(samples)?; + fn push_planar(rb_self: &Self, samples: PlanarSamples) -> Result<(), Error> { let mut analyzer = rb_self .analyzer .try_borrow_mut() From 41ceb2ea5f75227d1ddd3e6a14768547c3deac85 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Sat, 15 Aug 2026 14:15:12 +0900 Subject: [PATCH 078/122] refactor(ruby): use magnus::rb_sys::protect --- bindings/ruby/ext/ebur128_stream/src/memory_view.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bindings/ruby/ext/ebur128_stream/src/memory_view.rs b/bindings/ruby/ext/ebur128_stream/src/memory_view.rs index 00715d9..3577ec5 100644 --- a/bindings/ruby/ext/ebur128_stream/src/memory_view.rs +++ b/bindings/ruby/ext/ebur128_stream/src/memory_view.rs @@ -1,4 +1,4 @@ -use magnus::{Error, Ruby, Value}; +use magnus::{Error, Ruby, Value, rb_sys::protect}; use rb_sys::{ Qnil, VALUE, rb_memory_view_get, rb_memory_view_parse_item_format, rb_memory_view_release, rb_memory_view_t, @@ -117,7 +117,7 @@ pub struct MemoryView { impl Drop for MemoryView { // Causes segmentation fault in some cases. Needs the investigation. fn drop(&mut self) { - let _ = magnus::rb_sys::protect(|| { + let _ = protect(|| { // SAFETY: This guard is generated from successfully allocated rb_memory_view_t and drop() is called only once so it's not released more than once unsafe { rb_memory_view_release(&mut self.inner); @@ -135,7 +135,7 @@ impl MemoryView { let mut view = MaybeUninit::uninit(); let mut result = false; - magnus::rb_sys::protect(|| { + protect(|| { result = unsafe { rb_memory_view_get(obj, view.as_mut_ptr(), flags.into()) }; Qnil.into() })?; @@ -267,7 +267,7 @@ impl MemoryView { fn validate_format(ruby: &Ruby, format: &CStr) -> Result { let mut item_size = -1; - magnus::rb_sys::protect(|| { + protect(|| { item_size = unsafe { rb_memory_view_parse_item_format( format.as_ptr(), From 7a02d4c8b659f4d973fe57b1ab578778bb3e0be4 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Sat, 15 Aug 2026 14:16:12 +0900 Subject: [PATCH 079/122] refactor(ruby): use as_raw() instead of transmute() --- bindings/ruby/ext/ebur128_stream/src/memory_view.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/bindings/ruby/ext/ebur128_stream/src/memory_view.rs b/bindings/ruby/ext/ebur128_stream/src/memory_view.rs index 3577ec5..4de14b5 100644 --- a/bindings/ruby/ext/ebur128_stream/src/memory_view.rs +++ b/bindings/ruby/ext/ebur128_stream/src/memory_view.rs @@ -1,6 +1,9 @@ -use magnus::{Error, Ruby, Value, rb_sys::protect}; +use magnus::{ + Error, Ruby, Value, + rb_sys::{AsRawValue, protect}, +}; use rb_sys::{ - Qnil, VALUE, rb_memory_view_get, rb_memory_view_parse_item_format, rb_memory_view_release, + Qnil, rb_memory_view_get, rb_memory_view_parse_item_format, rb_memory_view_release, rb_memory_view_t, ruby_memory_view_flags::{ RUBY_MEMORY_VIEW_ANY_CONTIGUOUS, RUBY_MEMORY_VIEW_COLUMN_MAJOR, RUBY_MEMORY_VIEW_FORMAT, @@ -131,7 +134,7 @@ impl MemoryView { pub fn get(obj: Value, flags: Flags) -> Result { let ruby = Ruby::get_with(obj); - let obj = unsafe { std::mem::transmute::(obj) }; + let obj = obj.as_raw(); let mut view = MaybeUninit::uninit(); let mut result = false; From bedaa0b4858be1f125719131faf288645063910f Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Sat, 15 Aug 2026 14:36:10 +0900 Subject: [PATCH 080/122] feat(ruby): add snapshot module --- .../ruby/ext/ebur128_stream/src/snapshot.rs | 52 +++++++++++++++++++ bindings/ruby/lib/ebur128_stream.rb | 13 +++++ 2 files changed, 65 insertions(+) create mode 100644 bindings/ruby/ext/ebur128_stream/src/snapshot.rs diff --git a/bindings/ruby/ext/ebur128_stream/src/snapshot.rs b/bindings/ruby/ext/ebur128_stream/src/snapshot.rs new file mode 100644 index 0000000..ab3840a --- /dev/null +++ b/bindings/ruby/ext/ebur128_stream/src/snapshot.rs @@ -0,0 +1,52 @@ +use crate::Error; +use magnus::{Ruby, RModule, method, prelude::*}; +use ebur128_stream_rs as engine; + +#[magnus::wrap(class = "EBUR128Stream::Snapshot")] +pub(crate) struct Snapshot { + snapshot: engine::Snapshot, +} + +impl Snapshot { + fn momentary_lufs(&self) -> Option { + self.snapshot.momentary_lufs() + } + + fn short_term_lufs(&self) -> Option { + self.snapshot.short_term_lufs() + } + + fn integrated_lufs(&self) -> Option { + self.snapshot.integrated_lufs() + } + + fn true_peak_dbtp(&self) -> Option { + self.snapshot.true_peak_dbtp() + } + + fn loudness_range_lu(&self) -> Option { + self.snapshot.loudness_range_lu() + } + + fn programme_duration_seconds(&self) -> f64 { + self.snapshot.programme_duration_seconds() + } +} + +pub(crate) fn init(ruby: &Ruby, module: &RModule) -> Result<(), Error> { + let snapshot = module.define_class("Snapshot", ruby.class_object())?; + snapshot.define_method("momentary_lufs", method!(Snapshot::momentary_lufs, 0))?; + snapshot.define_method( + "short_term_lufs", + method!(Snapshot::short_term_lufs, 0), + )?; + snapshot.define_method("integrated_lufs", method!(Snapshot::integrated_lufs, 0))?; + snapshot.define_method("loudness_range_lu", method!(Snapshot::loudness_range_lu, 0))?; + snapshot.define_method("true_peak_dbtp", method!(Snapshot::true_peak_dbtp, 0))?; + snapshot.define_method( + "programme_duration_seconds", + method!(Snapshot::programme_duration_seconds, 0), + )?; + + Ok(()) +} \ No newline at end of file diff --git a/bindings/ruby/lib/ebur128_stream.rb b/bindings/ruby/lib/ebur128_stream.rb index 0c6cbae..5b740ef 100644 --- a/bindings/ruby/lib/ebur128_stream.rb +++ b/bindings/ruby/lib/ebur128_stream.rb @@ -23,6 +23,19 @@ def inspect end end + class Snapshot + include Reportable + + ATTRS = [ + :momentary_lufs, + :short_term_lufs, + :integrated_lufs, + :loudness_range_lu, + :true_peak_dbtp, + :programme_duration_seconds + ] + end + class Report include Reportable From 8d6afeec1b3b5f7d3475bb6ded98e314686dc18c Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Sat, 15 Aug 2026 14:56:39 +0900 Subject: [PATCH 081/122] feat(ruby): add Analyzer::snapshot() --- .../ruby/ext/ebur128_stream/src/analyzer.rs | 18 ++++++++++++++++-- bindings/ruby/ext/ebur128_stream/src/lib.rs | 3 +++ .../ruby/ext/ebur128_stream/src/snapshot.rs | 11 ++++------- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/bindings/ruby/ext/ebur128_stream/src/analyzer.rs b/bindings/ruby/ext/ebur128_stream/src/analyzer.rs index 11d7262..e124c7c 100644 --- a/bindings/ruby/ext/ebur128_stream/src/analyzer.rs +++ b/bindings/ruby/ext/ebur128_stream/src/analyzer.rs @@ -1,7 +1,7 @@ -use crate::{Error, InterleavedSamples, PlanarSamples, Report, parse_channels_arg}; +use crate::{Error, InterleavedSamples, PlanarSamples, Report, Snapshot, parse_channels_arg}; use ebur128_stream_rs as engine; use magnus::{ - Integer, RArray, RModule, Ruby, Symbol, TryConvert, Value, function, method, + Integer, Module, RArray, RModule, Ruby, Symbol, TryConvert, Value, function, method, prelude::*, scan_args::{get_kwargs, scan_args}, }; @@ -87,6 +87,19 @@ impl Analyzer { Ok(()) } + fn snapshot(rb_self: &Self) -> Result { + let mut analyzer = rb_self + .analyzer + .try_borrow_mut() + .map_err(|_| Error::runtime("analyzer aldready in use"))?; + let analyzer = analyzer + .as_mut() + .ok_or_else(|| Error::runtime("analyzer not initialized"))?; + let snapshot = analyzer.snapshot(); + + Ok(Snapshot { snapshot }) + } + fn finalize(rb_self: &Self) -> Result { let mut analyzer = rb_self .analyzer @@ -106,6 +119,7 @@ pub(crate) fn init(ruby: &Ruby, module: &RModule) -> Result<(), Error> { analyzer.define_singleton_method("new", function!(Analyzer::new, -1))?; analyzer.define_method("push_interleaved", method!(Analyzer::push_interleaved, 1))?; analyzer.define_method("push_planar", method!(Analyzer::push_planar, 1))?; + analyzer.define_method("snapshot", method!(Analyzer::snapshot, 0))?; analyzer.define_method("finalize", method!(Analyzer::finalize, 0))?; Ok(()) diff --git a/bindings/ruby/ext/ebur128_stream/src/lib.rs b/bindings/ruby/ext/ebur128_stream/src/lib.rs index bc1f810..d0ffc30 100644 --- a/bindings/ruby/ext/ebur128_stream/src/lib.rs +++ b/bindings/ruby/ext/ebur128_stream/src/lib.rs @@ -4,11 +4,13 @@ mod memory_view; mod normalize; mod report; mod samples; +mod snapshot; use crate::{ error::Error, report::Report, samples::{InterleavedSamples, PlanarSamples}, + snapshot::Snapshot, }; use ebur128_stream_rs as engine; use magnus::{RArray, Ruby, Symbol, TryConvert}; @@ -40,6 +42,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> { let ebur128_stream = ruby.define_module("EBUR128Stream")?; analyzer::init(ruby, &ebur128_stream)?; + snapshot::init(ruby, &ebur128_stream)?; report::init(ruby, &ebur128_stream)?; normalize::init(ruby, &ebur128_stream)?; diff --git a/bindings/ruby/ext/ebur128_stream/src/snapshot.rs b/bindings/ruby/ext/ebur128_stream/src/snapshot.rs index ab3840a..757bd7d 100644 --- a/bindings/ruby/ext/ebur128_stream/src/snapshot.rs +++ b/bindings/ruby/ext/ebur128_stream/src/snapshot.rs @@ -1,10 +1,10 @@ use crate::Error; -use magnus::{Ruby, RModule, method, prelude::*}; use ebur128_stream_rs as engine; +use magnus::{RModule, Ruby, method, prelude::*}; #[magnus::wrap(class = "EBUR128Stream::Snapshot")] pub(crate) struct Snapshot { - snapshot: engine::Snapshot, + pub(crate) snapshot: engine::Snapshot, } impl Snapshot { @@ -36,10 +36,7 @@ impl Snapshot { pub(crate) fn init(ruby: &Ruby, module: &RModule) -> Result<(), Error> { let snapshot = module.define_class("Snapshot", ruby.class_object())?; snapshot.define_method("momentary_lufs", method!(Snapshot::momentary_lufs, 0))?; - snapshot.define_method( - "short_term_lufs", - method!(Snapshot::short_term_lufs, 0), - )?; + snapshot.define_method("short_term_lufs", method!(Snapshot::short_term_lufs, 0))?; snapshot.define_method("integrated_lufs", method!(Snapshot::integrated_lufs, 0))?; snapshot.define_method("loudness_range_lu", method!(Snapshot::loudness_range_lu, 0))?; snapshot.define_method("true_peak_dbtp", method!(Snapshot::true_peak_dbtp, 0))?; @@ -49,4 +46,4 @@ pub(crate) fn init(ruby: &Ruby, module: &RModule) -> Result<(), Error> { )?; Ok(()) -} \ No newline at end of file +} From 84ef82381190c1ccaf9aa6b7160c36dac7c1a0ee Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Sat, 15 Aug 2026 14:56:58 +0900 Subject: [PATCH 082/122] test(ruby): add test for Snapshot --- bindings/ruby/test/test_snapshot.rb | 36 +++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 bindings/ruby/test/test_snapshot.rb diff --git a/bindings/ruby/test/test_snapshot.rb b/bindings/ruby/test/test_snapshot.rb new file mode 100644 index 0000000..1351534 --- /dev/null +++ b/bindings/ruby/test/test_snapshot.rb @@ -0,0 +1,36 @@ +require_relative "helper" + +class TestSnapshot < Test::Unit::TestCase + include EBUR128Stream + + def setup + sample_rate = 48_000 + analyzer = Analyzer.new(channels: [:left, :right], sample_rate:) + frame = 2 * Math::PI / (sample_rate / 4) + sample_rate.times do |n| + value = Math.cos(frame * n) * 10 + analyzer.push_interleaved [value, value] + end + @snapshot = analyzer.snapshot + end + + def test_attributes + assert_instance_of Float, @snapshot.momentary_lufs + assert_nil @snapshot.short_term_lufs + assert_instance_of Float, @snapshot.integrated_lufs + assert_nil @snapshot.loudness_range_lu + assert_instance_of Float, @snapshot.true_peak_dbtp + assert_equal 1.0, @snapshot.programme_duration_seconds + end + + def test_deconstruct_keys + deconstructed = @snapshot.deconstruct_keys(nil) + + assert_instance_of Float, deconstructed[:momentary_lufs] + assert_nil deconstructed[:short_term_lufs] + assert_instance_of Float, deconstructed[:integrated_lufs] + assert_nil deconstructed[:loudness_range_lu] + assert_instance_of Float, deconstructed[:true_peak_dbtp] + assert_equal 1.0, deconstructed[:programme_duration_seconds] + end +end \ No newline at end of file From 462d17df0c26118bb51770047c4acfed42336b29 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Sat, 15 Aug 2026 15:03:17 +0900 Subject: [PATCH 083/122] feat(ruby): add Analyzer#reset --- bindings/ruby/ext/ebur128_stream/src/analyzer.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/bindings/ruby/ext/ebur128_stream/src/analyzer.rs b/bindings/ruby/ext/ebur128_stream/src/analyzer.rs index e124c7c..a8b9a42 100644 --- a/bindings/ruby/ext/ebur128_stream/src/analyzer.rs +++ b/bindings/ruby/ext/ebur128_stream/src/analyzer.rs @@ -100,6 +100,19 @@ impl Analyzer { Ok(Snapshot { snapshot }) } + fn reset(rb_self: &Self) -> Result<(), Error> { + let mut analyzer = rb_self + .analyzer + .try_borrow_mut() + .map_err(|_| Error::runtime("analyzer aldready in use"))?; + let analyzer = analyzer + .as_mut() + .ok_or_else(|| Error::runtime("analyzer not initialized"))?; + analyzer.reset(); + + Ok(()) + } + fn finalize(rb_self: &Self) -> Result { let mut analyzer = rb_self .analyzer @@ -121,6 +134,7 @@ pub(crate) fn init(ruby: &Ruby, module: &RModule) -> Result<(), Error> { analyzer.define_method("push_planar", method!(Analyzer::push_planar, 1))?; analyzer.define_method("snapshot", method!(Analyzer::snapshot, 0))?; analyzer.define_method("finalize", method!(Analyzer::finalize, 0))?; + analyzer.define_method("reset", method!(Analyzer::reset, 0))?; Ok(()) } From 2451785ec850dcd3298ae52a9570f54890285236 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Sat, 15 Aug 2026 15:03:40 +0900 Subject: [PATCH 084/122] test(ruby): add test for Analyzer#reset --- bindings/ruby/test/test_ebur128_stream.rb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/bindings/ruby/test/test_ebur128_stream.rb b/bindings/ruby/test/test_ebur128_stream.rb index 071859a..27cc97f 100644 --- a/bindings/ruby/test/test_ebur128_stream.rb +++ b/bindings/ruby/test/test_ebur128_stream.rb @@ -94,4 +94,13 @@ class EBUR128StreamTest < Test::Unit::TestCase analyzer.finalize end end + + test "reset" do + analyzer = Analyzer.new(channels: [:left, :right], modes: [:all]) + analyzer.push_interleaved [1.0] * 48_000 * 2 + + assert_equal 1.0, analyzer.snapshot.programme_duration_seconds + analyzer.reset + assert_equal 0.0, analyzer.snapshot.programme_duration_seconds + end end From b0dd7a39491e6d5cc0174fa29e6cf219068f69c0 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Sat, 15 Aug 2026 15:06:52 +0900 Subject: [PATCH 085/122] fix(ruby): call PlanarSamples::try_convert() explicitely to avoid SEGV --- bindings/ruby/ext/ebur128_stream/src/analyzer.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bindings/ruby/ext/ebur128_stream/src/analyzer.rs b/bindings/ruby/ext/ebur128_stream/src/analyzer.rs index a8b9a42..26e359a 100644 --- a/bindings/ruby/ext/ebur128_stream/src/analyzer.rs +++ b/bindings/ruby/ext/ebur128_stream/src/analyzer.rs @@ -74,7 +74,8 @@ impl Analyzer { Ok(()) } - fn push_planar(rb_self: &Self, samples: PlanarSamples) -> Result<(), Error> { + fn push_planar(rb_self: &Self, samples: Value) -> Result<(), Error> { + let samples = PlanarSamples::try_convert(samples)?; let mut analyzer = rb_self .analyzer .try_borrow_mut() From 34c963d8892bcd62d18e959debb0132216724c69 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Sat, 15 Aug 2026 15:08:22 +0900 Subject: [PATCH 086/122] chore(ruby): remove unused useless assignment --- bindings/ruby/test/test_normalize_report.rb | 2 +- bindings/ruby/test/test_normalizer.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bindings/ruby/test/test_normalize_report.rb b/bindings/ruby/test/test_normalize_report.rb index b31d173..d4f8945 100644 --- a/bindings/ruby/test/test_normalize_report.rb +++ b/bindings/ruby/test/test_normalize_report.rb @@ -8,7 +8,7 @@ def setup normalizer = Normalizer.new(channels: [:left, :right], sample_rate:) frame = 2 * Math::PI / (sample_rate / 4) samples = sample_rate.times.flat_map do |n| - value = Math.cos(frame * n) * 10 + Math.cos(frame * n) * 10 end @report = normalizer.normalize_in_place(samples) end diff --git a/bindings/ruby/test/test_normalizer.rb b/bindings/ruby/test/test_normalizer.rb index 2cb410d..1a7b9b5 100644 --- a/bindings/ruby/test/test_normalizer.rb +++ b/bindings/ruby/test/test_normalizer.rb @@ -12,7 +12,7 @@ def test_normalize_in_place normalizer = Normalizer.new(channels: [:left, :right], sample_rate:) frame = 2 * Math::PI / (sample_rate / 4) samples = sample_rate.times.flat_map do |n| - value = Math.cos(frame * n) * 10 + Math.cos(frame * n) * 10 end before = samples.dup normalizer.normalize_in_place(samples) @@ -27,7 +27,7 @@ def test_normalize_in_place_memory_view normalizer = Normalizer.new(channels: [:left, :right], sample_rate:) frame = 2 * Math::PI / (sample_rate / 4) samples = sample_rate.times.flat_map do |n| - value = Math.cos(frame * n) * 10 + Math.cos(frame * n) * 10 end samples_mv = Numo::SFloat.cast(samples) normalizer.normalize_in_place(samples_mv) From c24c4532f55a2bc48c0bedab6fc351fbedda6dc8 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Sat, 15 Aug 2026 15:14:48 +0900 Subject: [PATCH 087/122] refactor(ruby): use self instead of rb_self --- .../ruby/ext/ebur128_stream/src/analyzer.rs | 20 ++++++------ .../ruby/ext/ebur128_stream/src/normalize.rs | 32 +++++++++---------- .../ruby/ext/ebur128_stream/src/report.rs | 24 +++++++------- 3 files changed, 38 insertions(+), 38 deletions(-) diff --git a/bindings/ruby/ext/ebur128_stream/src/analyzer.rs b/bindings/ruby/ext/ebur128_stream/src/analyzer.rs index 26e359a..f0a5ae9 100644 --- a/bindings/ruby/ext/ebur128_stream/src/analyzer.rs +++ b/bindings/ruby/ext/ebur128_stream/src/analyzer.rs @@ -62,10 +62,10 @@ impl Analyzer { } // Segmentation fault occurs if samples: InterleavedSamples in arguments - fn push_interleaved(rb_self: &Self, samples: Value) -> Result<(), Error> { + fn push_interleaved(&self, samples: Value) -> Result<(), Error> { let samples = InterleavedSamples::try_convert(samples)?; - let mut analyzer = rb_self.analyzer.try_borrow_mut().map_err(Error::runtime)?; + let mut analyzer = self.analyzer.try_borrow_mut().map_err(Error::runtime)?; let analyzer = analyzer .as_mut() .ok_or_else(|| Error::runtime("analyzer not initialized"))?; @@ -74,9 +74,9 @@ impl Analyzer { Ok(()) } - fn push_planar(rb_self: &Self, samples: Value) -> Result<(), Error> { + fn push_planar(&self, samples: Value) -> Result<(), Error> { let samples = PlanarSamples::try_convert(samples)?; - let mut analyzer = rb_self + let mut analyzer = self .analyzer .try_borrow_mut() .map_err(|_| Error::runtime("analyzer already in use"))?; @@ -88,8 +88,8 @@ impl Analyzer { Ok(()) } - fn snapshot(rb_self: &Self) -> Result { - let mut analyzer = rb_self + fn snapshot(&self) -> Result { + let mut analyzer = self .analyzer .try_borrow_mut() .map_err(|_| Error::runtime("analyzer aldready in use"))?; @@ -101,8 +101,8 @@ impl Analyzer { Ok(Snapshot { snapshot }) } - fn reset(rb_self: &Self) -> Result<(), Error> { - let mut analyzer = rb_self + fn reset(&self) -> Result<(), Error> { + let mut analyzer = self .analyzer .try_borrow_mut() .map_err(|_| Error::runtime("analyzer aldready in use"))?; @@ -114,8 +114,8 @@ impl Analyzer { Ok(()) } - fn finalize(rb_self: &Self) -> Result { - let mut analyzer = rb_self + fn finalize(&self) -> Result { + let mut analyzer = self .analyzer .try_borrow_mut() .map_err(|_| Error::runtime("analyzer already in use"))?; diff --git a/bindings/ruby/ext/ebur128_stream/src/normalize.rs b/bindings/ruby/ext/ebur128_stream/src/normalize.rs index b34a8f7..ca9a4b0 100644 --- a/bindings/ruby/ext/ebur128_stream/src/normalize.rs +++ b/bindings/ruby/ext/ebur128_stream/src/normalize.rs @@ -36,17 +36,17 @@ impl Normalizer { }) } - fn normalize_in_place(rb_self: &Self, samples: Value) -> Result { + fn normalize_in_place(&self, samples: Value) -> Result { let mut frames = InterleavedSamples::try_convert(samples)?; if !frames.is_writable() { return Err(Error::argument("samples not writable")); } let mut normalizer = - engine::normalize::Normalizer::new(rb_self.sample_rate, &rb_self.channels); - if let Some(target_lufs) = rb_self.target_lufs { + engine::normalize::Normalizer::new(self.sample_rate, &self.channels); + if let Some(target_lufs) = self.target_lufs { normalizer = normalizer.target_lufs(target_lufs); } - if let Some(true_peak_ceiling_dbtp) = rb_self.true_peak_ceiling_dbtp { + if let Some(true_peak_ceiling_dbtp) = self.true_peak_ceiling_dbtp { normalizer = normalizer.true_peak_ceiling_dbtp(true_peak_ceiling_dbtp); } @@ -65,28 +65,28 @@ struct NormalizeReport { } impl NormalizeReport { - fn measured_integrated_lufs(rb_self: &Self) -> Option { - rb_self.report.measured_integrated_lufs + fn measured_integrated_lufs(&self) -> Option { + self.report.measured_integrated_lufs } - fn measured_true_peak_dbtp(rb_self: &Self) -> Option { - rb_self.report.measured_true_peak_dbtp + fn measured_true_peak_dbtp(&self) -> Option { + self.report.measured_true_peak_dbtp } - fn target_lufs(rb_self: &Self) -> f64 { - rb_self.report.target_lufs + fn target_lufs(&self) -> f64 { + self.report.target_lufs } - fn true_peak_ceiling_dbtp(rb_self: &Self) -> Option { - rb_self.report.true_peak_ceiling_dbtp + fn true_peak_ceiling_dbtp(&self) -> Option { + self.report.true_peak_ceiling_dbtp } - fn applied_gain_db(rb_self: &Self) -> f64 { - rb_self.report.applied_gain_db + fn applied_gain_db(&self) -> f64 { + self.report.applied_gain_db } - fn limited_by_true_peak(rb_self: &Self) -> bool { - rb_self.report.limited_by_true_peak + fn limited_by_true_peak(&self) -> bool { + self.report.limited_by_true_peak } } diff --git a/bindings/ruby/ext/ebur128_stream/src/report.rs b/bindings/ruby/ext/ebur128_stream/src/report.rs index 8aa4844..8827a0a 100644 --- a/bindings/ruby/ext/ebur128_stream/src/report.rs +++ b/bindings/ruby/ext/ebur128_stream/src/report.rs @@ -8,28 +8,28 @@ pub(crate) struct Report { } impl Report { - fn integrated_lufs(rb_self: &Self) -> Option { - rb_self.report.integrated_lufs() + fn integrated_lufs(&self) -> Option { + self.report.integrated_lufs() } - fn loudness_range_lu(rb_self: &Self) -> Option { - rb_self.report.loudness_range_lu() + fn loudness_range_lu(&self) -> Option { + self.report.loudness_range_lu() } - fn true_peak_dbtp(rb_self: &Self) -> Option { - rb_self.report.true_peak_dbtp() + fn true_peak_dbtp(&self) -> Option { + self.report.true_peak_dbtp() } - fn momentary_max_lufs(rb_self: &Self) -> Option { - rb_self.report.momentary_max_lufs() + fn momentary_max_lufs(&self) -> Option { + self.report.momentary_max_lufs() } - fn short_term_max_lufs(rb_self: &Self) -> Option { - rb_self.report.short_term_max_lufs() + fn short_term_max_lufs(&self) -> Option { + self.report.short_term_max_lufs() } - fn programme_duration_seconds(rb_self: &Self) -> f64 { - rb_self.report.programme_duration_seconds() + fn programme_duration_seconds(&self) -> f64 { + self.report.programme_duration_seconds() } } From cbe9af039d65a8732b1b3d57d76b71c8a8027297 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Sat, 15 Aug 2026 15:35:46 +0900 Subject: [PATCH 088/122] refactor(ruby): add and use analyzer_mut() --- .../ruby/ext/ebur128_stream/src/analyzer.rs | 51 ++++++++----------- .../ruby/ext/ebur128_stream/src/normalize.rs | 3 +- 2 files changed, 21 insertions(+), 33 deletions(-) diff --git a/bindings/ruby/ext/ebur128_stream/src/analyzer.rs b/bindings/ruby/ext/ebur128_stream/src/analyzer.rs index f0a5ae9..cb04a60 100644 --- a/bindings/ruby/ext/ebur128_stream/src/analyzer.rs +++ b/bindings/ruby/ext/ebur128_stream/src/analyzer.rs @@ -5,7 +5,11 @@ use magnus::{ prelude::*, scan_args::{get_kwargs, scan_args}, }; -use std::{cell::RefCell, time::Duration}; +use std::{ + borrow::Borrow, + cell::{RefCell, RefMut}, + time::Duration, +}; #[magnus::wrap(class = "EBUR128Stream::Analyzer")] struct Analyzer { @@ -64,52 +68,27 @@ impl Analyzer { // Segmentation fault occurs if samples: InterleavedSamples in arguments fn push_interleaved(&self, samples: Value) -> Result<(), Error> { let samples = InterleavedSamples::try_convert(samples)?; - - let mut analyzer = self.analyzer.try_borrow_mut().map_err(Error::runtime)?; - let analyzer = analyzer - .as_mut() - .ok_or_else(|| Error::runtime("analyzer not initialized"))?; - analyzer.push_interleaved(samples.as_slice())?; + self.analyzer_mut()?.push_interleaved(samples.as_slice())?; Ok(()) } fn push_planar(&self, samples: Value) -> Result<(), Error> { let samples = PlanarSamples::try_convert(samples)?; - let mut analyzer = self - .analyzer - .try_borrow_mut() - .map_err(|_| Error::runtime("analyzer already in use"))?; - let analyzer = analyzer - .as_mut() - .ok_or_else(|| Error::runtime("analyzer not initialized"))?; - analyzer.push_planar(&samples.channel_slices())?; + self.analyzer_mut()? + .push_planar(&samples.channel_slices())?; Ok(()) } fn snapshot(&self) -> Result { - let mut analyzer = self - .analyzer - .try_borrow_mut() - .map_err(|_| Error::runtime("analyzer aldready in use"))?; - let analyzer = analyzer - .as_mut() - .ok_or_else(|| Error::runtime("analyzer not initialized"))?; - let snapshot = analyzer.snapshot(); + let snapshot = self.analyzer_mut()?.snapshot(); Ok(Snapshot { snapshot }) } fn reset(&self) -> Result<(), Error> { - let mut analyzer = self - .analyzer - .try_borrow_mut() - .map_err(|_| Error::runtime("analyzer aldready in use"))?; - let analyzer = analyzer - .as_mut() - .ok_or_else(|| Error::runtime("analyzer not initialized"))?; - analyzer.reset(); + self.analyzer_mut()?.reset(); Ok(()) } @@ -126,6 +105,16 @@ impl Analyzer { Ok(Report { report }) } + + fn analyzer_mut<'a>(&'a self) -> Result, Error> { + let analyzer = self + .analyzer + .try_borrow_mut() + .map_err(|_| Error::runtime("analyzer already in use"))?; + + RefMut::filter_map(analyzer, Option::as_mut) + .map_err(|_| Error::runtime("analyzer not initialized")) + } } pub(crate) fn init(ruby: &Ruby, module: &RModule) -> Result<(), Error> { diff --git a/bindings/ruby/ext/ebur128_stream/src/normalize.rs b/bindings/ruby/ext/ebur128_stream/src/normalize.rs index ca9a4b0..d89f7bc 100644 --- a/bindings/ruby/ext/ebur128_stream/src/normalize.rs +++ b/bindings/ruby/ext/ebur128_stream/src/normalize.rs @@ -41,8 +41,7 @@ impl Normalizer { if !frames.is_writable() { return Err(Error::argument("samples not writable")); } - let mut normalizer = - engine::normalize::Normalizer::new(self.sample_rate, &self.channels); + let mut normalizer = engine::normalize::Normalizer::new(self.sample_rate, &self.channels); if let Some(target_lufs) = self.target_lufs { normalizer = normalizer.target_lufs(target_lufs); } From 3556790a42102f5afd56ad1080ff2b7844eed01f Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Sat, 15 Aug 2026 16:50:46 +0900 Subject: [PATCH 089/122] feat(ruby): add Analyzer#sample_rate and #samples_per_block --- .../ruby/ext/ebur128_stream/src/analyzer.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/bindings/ruby/ext/ebur128_stream/src/analyzer.rs b/bindings/ruby/ext/ebur128_stream/src/analyzer.rs index cb04a60..a7e1658 100644 --- a/bindings/ruby/ext/ebur128_stream/src/analyzer.rs +++ b/bindings/ruby/ext/ebur128_stream/src/analyzer.rs @@ -6,8 +6,7 @@ use magnus::{ scan_args::{get_kwargs, scan_args}, }; use std::{ - borrow::Borrow, - cell::{RefCell, RefMut}, + cell::{RefCell, RefMut, Ref}, time::Duration, }; @@ -65,6 +64,14 @@ impl Analyzer { }) } + fn sample_rate(&self) -> Result { + Ok(self.analyzer()?.sample_rate()) + } + + fn samples_per_block(&self) -> Result { + Ok(self.analyzer()?.samples_per_block()) + } + // Segmentation fault occurs if samples: InterleavedSamples in arguments fn push_interleaved(&self, samples: Value) -> Result<(), Error> { let samples = InterleavedSamples::try_convert(samples)?; @@ -106,6 +113,12 @@ impl Analyzer { Ok(Report { report }) } + fn analyzer<'a>(&'a self) -> Result, Error> { + let analyzer = self.analyzer.try_borrow().map_err(|_| Error::runtime("analyzer already in use"))?; + + Ref::filter_map(analyzer, Option::as_ref).map_err(|_| Error::runtime("analyzer not initialized")) + } + fn analyzer_mut<'a>(&'a self) -> Result, Error> { let analyzer = self .analyzer @@ -120,6 +133,8 @@ impl Analyzer { pub(crate) fn init(ruby: &Ruby, module: &RModule) -> Result<(), Error> { let analyzer = module.define_class("Analyzer", ruby.class_object())?; analyzer.define_singleton_method("new", function!(Analyzer::new, -1))?; + analyzer.define_method("sample_rate", method!(Analyzer::sample_rate, 0))?; + analyzer.define_method("samples_per_block", method!(Analyzer::samples_per_block, 0))?; analyzer.define_method("push_interleaved", method!(Analyzer::push_interleaved, 1))?; analyzer.define_method("push_planar", method!(Analyzer::push_planar, 1))?; analyzer.define_method("snapshot", method!(Analyzer::snapshot, 0))?; From 20c19a58a4dc725b19d540008cdaa873323240c2 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Sun, 16 Aug 2026 02:01:17 +0900 Subject: [PATCH 090/122] style(ruby): format --- bindings/ruby/ext/ebur128_stream/src/analyzer.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/bindings/ruby/ext/ebur128_stream/src/analyzer.rs b/bindings/ruby/ext/ebur128_stream/src/analyzer.rs index a7e1658..3431a34 100644 --- a/bindings/ruby/ext/ebur128_stream/src/analyzer.rs +++ b/bindings/ruby/ext/ebur128_stream/src/analyzer.rs @@ -6,7 +6,7 @@ use magnus::{ scan_args::{get_kwargs, scan_args}, }; use std::{ - cell::{RefCell, RefMut, Ref}, + cell::{Ref, RefCell, RefMut}, time::Duration, }; @@ -114,9 +114,13 @@ impl Analyzer { } fn analyzer<'a>(&'a self) -> Result, Error> { - let analyzer = self.analyzer.try_borrow().map_err(|_| Error::runtime("analyzer already in use"))?; + let analyzer = self + .analyzer + .try_borrow() + .map_err(|_| Error::runtime("analyzer already in use"))?; - Ref::filter_map(analyzer, Option::as_ref).map_err(|_| Error::runtime("analyzer not initialized")) + Ref::filter_map(analyzer, Option::as_ref) + .map_err(|_| Error::runtime("analyzer not initialized")) } fn analyzer_mut<'a>(&'a self) -> Result, Error> { From eb1b71a8bef85f1db3f55a7ef5d53d3d55d06513 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Sun, 16 Aug 2026 02:01:56 +0900 Subject: [PATCH 091/122] feat(ruby): add Channels --- bindings/ruby/ext/ebur128_stream/src/lib.rs | 51 ++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/bindings/ruby/ext/ebur128_stream/src/lib.rs b/bindings/ruby/ext/ebur128_stream/src/lib.rs index d0ffc30..8638a37 100644 --- a/bindings/ruby/ext/ebur128_stream/src/lib.rs +++ b/bindings/ruby/ext/ebur128_stream/src/lib.rs @@ -13,7 +13,7 @@ use crate::{ snapshot::Snapshot, }; use ebur128_stream_rs as engine; -use magnus::{RArray, Ruby, Symbol, TryConvert}; +use std::ops::Deref; // can make channels a slice? pub(crate) fn parse_channels_arg(channels: RArray) -> Result, Error> { @@ -37,6 +37,55 @@ pub(crate) fn parse_channels_arg(channels: RArray) -> Result, +} + +impl Deref for Channels { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl TryConvert for Channels { + fn try_convert(val: Value) -> Result { + use engine::Channel::*; + + let array = RArray::try_convert(val)?; + let result: Result, magnus::Error> = array + .into_iter() + .map(|value| { + let ch = Symbol::try_convert(value)?; + match ch.name()?.as_ref() { + "left" => Ok(Left), + "right" => Ok(Right), + "center" => Ok(Center), + "left_surround" => Ok(LeftSurround), + "right_surround" => Ok(RightSurround), + "lfe" => Ok(Lfe), + "other" => Ok(Other), + _ => { + let ruby = Ruby::get_with(val); + Err(magnus::Error::new( + ruby.exception_runtime_error(), + format!("unknown channel: {ch}"), + )) + } + } + }) + .collect(); + Ok(Self { inner: result? }) + } +} + +impl Channels { + fn into_boxed_slice(self) -> Box<[engine::Channel]> { + self.inner.into_boxed_slice() + } +} + #[magnus::init] fn init(ruby: &Ruby) -> Result<(), Error> { let ebur128_stream = ruby.define_module("EBUR128Stream")?; From b0f74be56b96062476acdf06ef7769380c9c5dc0 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Sun, 16 Aug 2026 02:33:08 +0900 Subject: [PATCH 092/122] refactor(ruby): use Channels instead of parse_channels_arg() --- bindings/ruby/ext/ebur128_stream/src/analyzer.rs | 14 +++++++------- bindings/ruby/ext/ebur128_stream/src/normalize.rs | 8 +++----- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/bindings/ruby/ext/ebur128_stream/src/analyzer.rs b/bindings/ruby/ext/ebur128_stream/src/analyzer.rs index 3431a34..8a3c5d5 100644 --- a/bindings/ruby/ext/ebur128_stream/src/analyzer.rs +++ b/bindings/ruby/ext/ebur128_stream/src/analyzer.rs @@ -1,4 +1,4 @@ -use crate::{Error, InterleavedSamples, PlanarSamples, Report, Snapshot, parse_channels_arg}; +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, @@ -18,15 +18,15 @@ struct Analyzer { impl Analyzer { fn new(args: &[Value]) -> Result { let args = scan_args::<(), (), (), (), _, ()>(args)?; - let kws = get_kwargs::<_, (RArray,), (Option, Option, Option), ()>( - args.keywords, - &["channels"], - &["sample_rate", "modes", "expected_duration"], - )?; + let kws = + get_kwargs::<_, (Channels,), (Option, Option, Option), ()>( + args.keywords, + &["channels"], + &["sample_rate", "modes", "expected_duration"], + )?; let (channels,) = kws.required; let (sample_rate, modes, expected_duration) = kws.optional; - let channels = parse_channels_arg(channels)?; let mut builder = engine::AnalyzerBuilder::new().channels(&channels); if let Some(sample_rate) = sample_rate { diff --git a/bindings/ruby/ext/ebur128_stream/src/normalize.rs b/bindings/ruby/ext/ebur128_stream/src/normalize.rs index d89f7bc..e946a3e 100644 --- a/bindings/ruby/ext/ebur128_stream/src/normalize.rs +++ b/bindings/ruby/ext/ebur128_stream/src/normalize.rs @@ -1,7 +1,7 @@ -use crate::{error::Error, parse_channels_arg, samples::InterleavedSamples}; +use crate::{Channels, error::Error, samples::InterleavedSamples}; use ebur128_stream_rs as engine; use magnus::{ - RArray, RModule, Ruby, TryConvert, Value, function, method, + RModule, Ruby, TryConvert, Value, function, method, prelude::*, scan_args::{get_kwargs, scan_args}, }; @@ -18,7 +18,7 @@ struct Normalizer { impl Normalizer { fn new(args: &[Value]) -> Result { let args = scan_args::<(), (), (), (), _, ()>(args)?; - let kws = get_kwargs::<_, (u32, RArray), (Option, Option), ()>( + let kws = get_kwargs::<_, (u32, Channels), (Option, Option), ()>( args.keywords, &["sample_rate", "channels"], &["target_lufs", "true_peak_ceiling_dbtp"], @@ -26,8 +26,6 @@ impl Normalizer { let (sample_rate, channels) = kws.required; let (target_lufs, true_peak_ceiling_dbtp) = kws.optional; - let channels = parse_channels_arg(channels)?; - Ok(Self { sample_rate, channels: channels.into_boxed_slice(), From 7b2b76c21a1734559145726af15c42b41cba8fda Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Sun, 16 Aug 2026 03:01:21 +0900 Subject: [PATCH 093/122] chore(ruby): remove unsed function --- bindings/ruby/ext/ebur128_stream/src/lib.rs | 23 +-------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/bindings/ruby/ext/ebur128_stream/src/lib.rs b/bindings/ruby/ext/ebur128_stream/src/lib.rs index 8638a37..1fac322 100644 --- a/bindings/ruby/ext/ebur128_stream/src/lib.rs +++ b/bindings/ruby/ext/ebur128_stream/src/lib.rs @@ -13,30 +13,9 @@ use crate::{ snapshot::Snapshot, }; use ebur128_stream_rs as engine; +use magnus::{RArray, Ruby, Symbol, TryConvert, Value}; use std::ops::Deref; -// can make channels a slice? -pub(crate) fn parse_channels_arg(channels: RArray) -> Result, Error> { - use engine::Channel::*; - - channels - .into_iter() - .map(|value| { - let ch = Symbol::try_convert(value)?; - match ch.name()?.as_ref() { - "left" => Ok(Left), - "right" => Ok(Right), - "center" => Ok(Center), - "left_surround" => Ok(LeftSurround), - "right_surround" => Ok(RightSurround), - "lfe" => Ok(Lfe), - "other" => Ok(Other), - _ => Err(Error::argument(format!("unknown channel: {ch}")))?, - } - }) - .collect() -} - pub(crate) struct Channels { inner: Vec, } From 655ce92b62a4546ba05f0ab20b4479384e3d04c7 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Sun, 16 Aug 2026 03:35:17 +0900 Subject: [PATCH 094/122] refactor(ruby): extract Channel --- bindings/ruby/ext/ebur128_stream/src/lib.rs | 62 ++++++++++++--------- 1 file changed, 36 insertions(+), 26 deletions(-) diff --git a/bindings/ruby/ext/ebur128_stream/src/lib.rs b/bindings/ruby/ext/ebur128_stream/src/lib.rs index 1fac322..911a98e 100644 --- a/bindings/ruby/ext/ebur128_stream/src/lib.rs +++ b/bindings/ruby/ext/ebur128_stream/src/lib.rs @@ -16,6 +16,36 @@ use ebur128_stream_rs as engine; use magnus::{RArray, Ruby, Symbol, TryConvert, Value}; use std::ops::Deref; +struct Channel(engine::Channel); + +impl From for engine::Channel { + fn from(value: Channel) -> Self { + value.0 + } +} + +impl TryConvert for Channel { + fn try_convert(val: Value) -> Result { + let channel = Symbol::try_convert(val)?; + Ok(match channel.name()?.as_ref() { + "left" => Self(engine::Channel::Left), + "right" => Self(engine::Channel::Right), + "center" => Self(engine::Channel::Center), + "left_surround" => Self(engine::Channel::LeftSurround), + "right_surround" => Self(engine::Channel::RightSurround), + "lfe" => Self(engine::Channel::Lfe), + "other" => Self(engine::Channel::Other), + _ => { + let ruby = Ruby::get_with(val); + return Err(magnus::Error::new( + ruby.exception_arg_error(), + format!("unknown channel: {val}"), + )); + } + }) + } +} + pub(crate) struct Channels { inner: Vec, } @@ -30,32 +60,12 @@ impl Deref for Channels { impl TryConvert for Channels { fn try_convert(val: Value) -> Result { - use engine::Channel::*; - - let array = RArray::try_convert(val)?; - let result: Result, magnus::Error> = array - .into_iter() - .map(|value| { - let ch = Symbol::try_convert(value)?; - match ch.name()?.as_ref() { - "left" => Ok(Left), - "right" => Ok(Right), - "center" => Ok(Center), - "left_surround" => Ok(LeftSurround), - "right_surround" => Ok(RightSurround), - "lfe" => Ok(Lfe), - "other" => Ok(Other), - _ => { - let ruby = Ruby::get_with(val); - Err(magnus::Error::new( - ruby.exception_runtime_error(), - format!("unknown channel: {ch}"), - )) - } - } - }) - .collect(); - Ok(Self { inner: result? }) + Ok(Self { + inner: RArray::try_convert(val)? + .into_iter() + .map(|value| Ok(Channel::try_convert(value)?.into())) + .collect::, magnus::Error>>()?, + }) } } From 9bef4a7532a1b202c062a25aad2a0f3e995872c9 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Sun, 16 Aug 2026 05:46:50 +0900 Subject: [PATCH 095/122] feat(ruby): add Analyzer#channels --- .../ruby/ext/ebur128_stream/src/analyzer.rs | 5 ++++ bindings/ruby/ext/ebur128_stream/src/lib.rs | 25 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/bindings/ruby/ext/ebur128_stream/src/analyzer.rs b/bindings/ruby/ext/ebur128_stream/src/analyzer.rs index 8a3c5d5..7cee6b3 100644 --- a/bindings/ruby/ext/ebur128_stream/src/analyzer.rs +++ b/bindings/ruby/ext/ebur128_stream/src/analyzer.rs @@ -68,6 +68,10 @@ impl Analyzer { Ok(self.analyzer()?.sample_rate()) } + fn channels(ruby: &Ruby, rb_self: &Self) -> Result { + Ok(Channels::from(rb_self.analyzer()?.channels()).try_into_rarray(ruby)?) + } + fn samples_per_block(&self) -> Result { Ok(self.analyzer()?.samples_per_block()) } @@ -138,6 +142,7 @@ pub(crate) fn init(ruby: &Ruby, module: &RModule) -> Result<(), Error> { let analyzer = module.define_class("Analyzer", ruby.class_object())?; analyzer.define_singleton_method("new", function!(Analyzer::new, -1))?; analyzer.define_method("sample_rate", method!(Analyzer::sample_rate, 0))?; + analyzer.define_method("channels", method!(Analyzer::channels, 0))?; analyzer.define_method("samples_per_block", method!(Analyzer::samples_per_block, 0))?; analyzer.define_method("push_interleaved", method!(Analyzer::push_interleaved, 1))?; analyzer.define_method("push_planar", method!(Analyzer::push_planar, 1))?; diff --git a/bindings/ruby/ext/ebur128_stream/src/lib.rs b/bindings/ruby/ext/ebur128_stream/src/lib.rs index 911a98e..595a19f 100644 --- a/bindings/ruby/ext/ebur128_stream/src/lib.rs +++ b/bindings/ruby/ext/ebur128_stream/src/lib.rs @@ -58,6 +58,12 @@ impl Deref for Channels { } } +impl<'a> From<&'a [engine::Channel]> for Channels { + fn from(value: &'a [engine::Channel]) -> Self { + Self { inner: value.to_vec().into_iter().collect() } + } +} + impl TryConvert for Channels { fn try_convert(val: Value) -> Result { Ok(Self { @@ -70,6 +76,25 @@ impl TryConvert for Channels { } impl Channels { + fn try_into_rarray(&self, ruby: &Ruby) -> Result { + let syms = self.inner.iter().map(|channel| { + use engine::Channel::*; + + let str = match channel { + Left => "left", + Right => "right", + Center => "center", + LeftSurround => "left_surround", + RightSurround => "right_surround", + Lfe => "lfe", + Other => "other", + _ => return Err(magnus::Error::new(ruby.exception_runtime_error(), "couldn't convert to Symbol: {channel}")) + }; + Ok(ruby.to_symbol(str)) + }).collect::, magnus::Error>>()?; + Ok(ruby.ary_new_from_values(&syms)) + } + fn into_boxed_slice(self) -> Box<[engine::Channel]> { self.inner.into_boxed_slice() } From d5e501f129c94ecf3689c56ca5136395aa9d7f7b Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Sun, 16 Aug 2026 06:14:03 +0900 Subject: [PATCH 096/122] feat(ruby): add Analyzer#modes --- bindings/ruby/ext/ebur128_stream/src/analyzer.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/bindings/ruby/ext/ebur128_stream/src/analyzer.rs b/bindings/ruby/ext/ebur128_stream/src/analyzer.rs index 7cee6b3..c1ef705 100644 --- a/bindings/ruby/ext/ebur128_stream/src/analyzer.rs +++ b/bindings/ruby/ext/ebur128_stream/src/analyzer.rs @@ -72,6 +72,16 @@ impl Analyzer { Ok(Channels::from(rb_self.analyzer()?.channels()).try_into_rarray(ruby)?) } + fn modes<'a>(ruby: &Ruby, rb_self: &'a Self) -> Result { + let syms = rb_self + .analyzer()? + .modes() + .iter_names() + .map(|mode| ruby.to_symbol(mode.0)) + .collect::>(); + Ok(ruby.ary_new_from_values(&syms)) + } + fn samples_per_block(&self) -> Result { Ok(self.analyzer()?.samples_per_block()) } @@ -143,6 +153,7 @@ pub(crate) fn init(ruby: &Ruby, module: &RModule) -> Result<(), Error> { analyzer.define_singleton_method("new", function!(Analyzer::new, -1))?; analyzer.define_method("sample_rate", method!(Analyzer::sample_rate, 0))?; analyzer.define_method("channels", method!(Analyzer::channels, 0))?; + analyzer.define_method("modes", method!(Analyzer::modes, 0))?; analyzer.define_method("samples_per_block", method!(Analyzer::samples_per_block, 0))?; analyzer.define_method("push_interleaved", method!(Analyzer::push_interleaved, 1))?; analyzer.define_method("push_planar", method!(Analyzer::push_planar, 1))?; From 7eb6b50e20a69ca7c2cd088829141e154f052a24 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Sun, 16 Aug 2026 06:14:20 +0900 Subject: [PATCH 097/122] style(ruby): format --- .../ruby/ext/ebur128_stream/src/analyzer.rs | 2 +- bindings/ruby/ext/ebur128_stream/src/lib.rs | 41 ++++++++++++------- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/bindings/ruby/ext/ebur128_stream/src/analyzer.rs b/bindings/ruby/ext/ebur128_stream/src/analyzer.rs index c1ef705..2127209 100644 --- a/bindings/ruby/ext/ebur128_stream/src/analyzer.rs +++ b/bindings/ruby/ext/ebur128_stream/src/analyzer.rs @@ -77,7 +77,7 @@ impl Analyzer { .analyzer()? .modes() .iter_names() - .map(|mode| ruby.to_symbol(mode.0)) + .map(|mode| ruby.to_symbol(mode.0.to_lowercase())) .collect::>(); Ok(ruby.ary_new_from_values(&syms)) } diff --git a/bindings/ruby/ext/ebur128_stream/src/lib.rs b/bindings/ruby/ext/ebur128_stream/src/lib.rs index 595a19f..267f9a3 100644 --- a/bindings/ruby/ext/ebur128_stream/src/lib.rs +++ b/bindings/ruby/ext/ebur128_stream/src/lib.rs @@ -60,7 +60,9 @@ impl Deref for Channels { impl<'a> From<&'a [engine::Channel]> for Channels { fn from(value: &'a [engine::Channel]) -> Self { - Self { inner: value.to_vec().into_iter().collect() } + Self { + inner: value.to_vec().into_iter().collect(), + } } } @@ -77,21 +79,30 @@ impl TryConvert for Channels { impl Channels { fn try_into_rarray(&self, ruby: &Ruby) -> Result { - let syms = self.inner.iter().map(|channel| { - use engine::Channel::*; + let syms = self + .inner + .iter() + .map(|channel| { + use engine::Channel::*; - let str = match channel { - Left => "left", - Right => "right", - Center => "center", - LeftSurround => "left_surround", - RightSurround => "right_surround", - Lfe => "lfe", - Other => "other", - _ => return Err(magnus::Error::new(ruby.exception_runtime_error(), "couldn't convert to Symbol: {channel}")) - }; - Ok(ruby.to_symbol(str)) - }).collect::, magnus::Error>>()?; + let str = match channel { + Left => "left", + Right => "right", + Center => "center", + LeftSurround => "left_surround", + RightSurround => "right_surround", + Lfe => "lfe", + Other => "other", + _ => { + return Err(magnus::Error::new( + ruby.exception_runtime_error(), + "couldn't convert to Symbol: {channel}", + )); + } + }; + Ok(ruby.to_symbol(str)) + }) + .collect::, magnus::Error>>()?; Ok(ruby.ary_new_from_values(&syms)) } From bb913ae1538ce5384643410f8655ba091e913f24 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Sun, 16 Aug 2026 06:17:54 +0900 Subject: [PATCH 098/122] test(ruby): add test for Analyzer#modes --- bindings/ruby/test/test_ebur128_stream.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bindings/ruby/test/test_ebur128_stream.rb b/bindings/ruby/test/test_ebur128_stream.rb index 27cc97f..edd099f 100644 --- a/bindings/ruby/test/test_ebur128_stream.rb +++ b/bindings/ruby/test/test_ebur128_stream.rb @@ -103,4 +103,10 @@ class EBUR128StreamTest < Test::Unit::TestCase analyzer.reset assert_equal 0.0, analyzer.snapshot.programme_duration_seconds end + + test "modes" do + analyzer = Analyzer.new(channels: [:left, :right], modes: [:momentary, :integrated]) + + assert_equal [:integrated, :momentary], analyzer.modes + end end From 131136ba2b1d09042073fb3e93a628d14fa11875 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Sun, 16 Aug 2026 06:53:29 +0900 Subject: [PATCH 099/122] feat(ruby): add Reportable#== --- bindings/ruby/lib/ebur128_stream.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bindings/ruby/lib/ebur128_stream.rb b/bindings/ruby/lib/ebur128_stream.rb index 5b740ef..d5ce661 100644 --- a/bindings/ruby/lib/ebur128_stream.rb +++ b/bindings/ruby/lib/ebur128_stream.rb @@ -21,6 +21,10 @@ def inspect attrs: self.class::ATTRS.collect {|attr| "#{attr}=#{send(attr).inspect}"}.join(" ") } end + + def ==(other) + deconstruct_keys(nil) == other.deconstruct_keys(nil) + end end class Snapshot From 0b29e47ff270ef9721fd962b1940affdcf9af545 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Sun, 16 Aug 2026 06:54:01 +0900 Subject: [PATCH 100/122] test(ruby): add test to see diff of Array and MemoryView --- bindings/ruby/test/helper.rb | 10 ++++++++++ bindings/ruby/test/test_ebur128_stream.rb | 15 +++++++++++++++ bindings/ruby/test/test_normalizer.rb | 11 ++++++++--- 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/bindings/ruby/test/helper.rb b/bindings/ruby/test/helper.rb index ba4683a..59a6354 100644 --- a/bindings/ruby/test/helper.rb +++ b/bindings/ruby/test/helper.rb @@ -6,3 +6,13 @@ require "test-unit" require "numo/narray/alt" require "ndav/numo/narray" + +class Test::Unit::TestCase + def generate_samples + sample_rate = 48_000 + frame = 2 * Math::PI / (sample_rate / 4) + sample_rate.times.flat_map do |n| + Math.cos(frame * n) * 10 + end + end +end diff --git a/bindings/ruby/test/test_ebur128_stream.rb b/bindings/ruby/test/test_ebur128_stream.rb index edd099f..fb71b5c 100644 --- a/bindings/ruby/test/test_ebur128_stream.rb +++ b/bindings/ruby/test/test_ebur128_stream.rb @@ -109,4 +109,19 @@ class EBUR128StreamTest < Test::Unit::TestCase assert_equal [:integrated, :momentary], analyzer.modes end + + test "push_interleaved Array and MemoryView" do + samples = generate_samples + + analyzer_ary = Analyzer.new(channels: [:left, :right], modes: [:all]) + analyzer_ary.push_interleaved samples + report_ary = analyzer_ary.finalize + + samples_mv = Numo::SFloat.cast(samples) + analyzer_mv = Analyzer.new(channels: [:left, :right], modes: [:all]) + analyzer_mv.push_interleaved samples_mv + report_mv = analyzer_mv.finalize + + assert_equal report_ary, report_mv + end end diff --git a/bindings/ruby/test/test_normalizer.rb b/bindings/ruby/test/test_normalizer.rb index 1a7b9b5..753fb81 100644 --- a/bindings/ruby/test/test_normalizer.rb +++ b/bindings/ruby/test/test_normalizer.rb @@ -36,9 +36,14 @@ def test_normalize_in_place_memory_view samples_mv.to_a != samples end - normalizer.normalize_in_place(samples) - assert do - samples_mv == samples + samples2 = sample_rate.times.flat_map do |n| + Math.cos(frame * n) * 10 + end + normalizer2 = Normalizer.new(channels: [:left, :right], sample_rate:) + normalizer2.normalize_in_place(samples2) + assert_equal samples2.length, samples_mv.length + samples_mv.each_with_index do |sample, i| + assert_in_delta sample, samples2[i], 0.001, "at sample #{i}" end end end From 05e80e5fa61a33be41a8ac40546b223bfd31097d Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Sun, 16 Aug 2026 08:17:25 +0900 Subject: [PATCH 101/122] chore(ruby): add sync_version task --- bindings/ruby/Rakefile | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/bindings/ruby/Rakefile b/bindings/ruby/Rakefile index 197165d..65036a2 100644 --- a/bindings/ruby/Rakefile +++ b/bindings/ruby/Rakefile @@ -11,3 +11,10 @@ cargo "ebur128_stream" Rake::TestTask.new(test: :cargo) 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] From 5a4c17a277176a045dd58e82354fcd835e30b838 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Sun, 16 Aug 2026 12:38:48 +0900 Subject: [PATCH 102/122] fix(ruby): initialize MemoryView with 0 --- bindings/ruby/ext/ebur128_stream/src/memory_view.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bindings/ruby/ext/ebur128_stream/src/memory_view.rs b/bindings/ruby/ext/ebur128_stream/src/memory_view.rs index 4de14b5..237fa85 100644 --- a/bindings/ruby/ext/ebur128_stream/src/memory_view.rs +++ b/bindings/ruby/ext/ebur128_stream/src/memory_view.rs @@ -118,7 +118,6 @@ pub struct MemoryView { } impl Drop for MemoryView { - // Causes segmentation fault in some cases. Needs the investigation. fn drop(&mut self) { let _ = protect(|| { // SAFETY: This guard is generated from successfully allocated rb_memory_view_t and drop() is called only once so it's not released more than once @@ -135,7 +134,7 @@ impl MemoryView { let ruby = Ruby::get_with(obj); let obj = obj.as_raw(); - let mut view = MaybeUninit::uninit(); + let mut view = MaybeUninit::zeroed(); let mut result = false; protect(|| { From a02646447e2598f6e06a1ca6e4a7bbaabdd56db4 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Sun, 16 Aug 2026 12:39:55 +0900 Subject: [PATCH 103/122] feat(ruby): complete gemspec --- bindings/ruby/ebur128_stream.gemspec | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/bindings/ruby/ebur128_stream.gemspec b/bindings/ruby/ebur128_stream.gemspec index 32a1d7d..0499db9 100644 --- a/bindings/ruby/ebur128_stream.gemspec +++ b/bindings/ruby/ebur128_stream.gemspec @@ -9,14 +9,14 @@ Gem::Specification.new do |spec| spec.email = ["KitaitiMakoto@gmail.com"] spec.licenses = ["Apache-2.0", "MIT"] - spec.summary = "TODO: Write a short summary, because RubyGems requires one." - spec.description = "TODO: Write a longer description or delete this line." - spec.homepage = "TODO: Put your gem's website or public repo URL here." + 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/vanjamodrinjak21/ebur128-stream" spec.required_ruby_version = ">= 3.2.0" - spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'" + spec.metadata["allowed_push_host"] = "https://rubygems.org" spec.metadata["homepage_uri"] = spec.homepage - spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here." - spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here." + spec.metadata["source_code_uri"] = "https://github.com/KitaitiMakoto/ebur128-stream/tree/main/bindings/ruby" + spec.metadata["changelog_uri"] = "https://github.com/KitaitiMakoto/ebur128-stream/tree/main/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 @@ -28,13 +28,9 @@ Gem::Specification.new do |spec| # 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).reject do |f| - (f == gemspec) || - f.start_with?(*%w[bin/ Gemfile .gitignore test/]) - end - end - spec.bindir = "exe" - spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } + 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"] From 00bb83e406a8b32aa78507319334177f5716b9a2 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Sun, 16 Aug 2026 12:40:59 +0900 Subject: [PATCH 104/122] doc(ruby): add license files in gem package --- bindings/ruby/Rakefile | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/bindings/ruby/Rakefile b/bindings/ruby/Rakefile index 65036a2..57084c4 100644 --- a/bindings/ruby/Rakefile +++ b/bindings/ruby/Rakefile @@ -3,12 +3,23 @@ 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 @@ -17,4 +28,4 @@ task sync_version: ["lib/ebur128_stream/version.rb", "ext/ebur128_stream/Cargo.t sh "cargo", "set-version", "--manifest-path", t.sources[1], "--offline", EBUR128Stream::VERSION end -task build: [:sync_version, :cargo] +task build: [:sync_version, :cargo] + LICENSE_FILES From fa7383a01f106a2e14eb90d3b8eca3138e8ddefa Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Mon, 17 Aug 2026 07:53:20 +0900 Subject: [PATCH 105/122] refactor(ruby): use Interleaved/PlanaerSamples as method argument --- .../ruby/ext/ebur128_stream/src/analyzer.rs | 17 +++++------------ .../ruby/ext/ebur128_stream/src/normalize.rs | 11 +++++------ 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/bindings/ruby/ext/ebur128_stream/src/analyzer.rs b/bindings/ruby/ext/ebur128_stream/src/analyzer.rs index 2127209..2f7eab1 100644 --- a/bindings/ruby/ext/ebur128_stream/src/analyzer.rs +++ b/bindings/ruby/ext/ebur128_stream/src/analyzer.rs @@ -86,20 +86,13 @@ impl Analyzer { Ok(self.analyzer()?.samples_per_block()) } - // Segmentation fault occurs if samples: InterleavedSamples in arguments - fn push_interleaved(&self, samples: Value) -> Result<(), Error> { - let samples = InterleavedSamples::try_convert(samples)?; - self.analyzer_mut()?.push_interleaved(samples.as_slice())?; - - Ok(()) + fn push_interleaved(&self, samples: InterleavedSamples) -> Result<(), Error> { + Ok(self.analyzer_mut()?.push_interleaved(samples.as_slice())?) } - fn push_planar(&self, samples: Value) -> Result<(), Error> { - let samples = PlanarSamples::try_convert(samples)?; - self.analyzer_mut()? - .push_planar(&samples.channel_slices())?; - - Ok(()) + fn push_planar(&self, samples: PlanarSamples) -> Result<(), Error> { + Ok(self.analyzer_mut()? + .push_planar(&samples.channel_slices())?) } fn snapshot(&self) -> Result { diff --git a/bindings/ruby/ext/ebur128_stream/src/normalize.rs b/bindings/ruby/ext/ebur128_stream/src/normalize.rs index e946a3e..89ebea0 100644 --- a/bindings/ruby/ext/ebur128_stream/src/normalize.rs +++ b/bindings/ruby/ext/ebur128_stream/src/normalize.rs @@ -1,7 +1,7 @@ use crate::{Channels, error::Error, samples::InterleavedSamples}; use ebur128_stream_rs as engine; use magnus::{ - RModule, Ruby, TryConvert, Value, function, method, + RModule, Ruby, Value, function, method, prelude::*, scan_args::{get_kwargs, scan_args}, }; @@ -34,9 +34,8 @@ impl Normalizer { }) } - fn normalize_in_place(&self, samples: Value) -> Result { - let mut frames = InterleavedSamples::try_convert(samples)?; - if !frames.is_writable() { + fn normalize_in_place(&self, mut samples: InterleavedSamples) -> Result { + if !samples.is_writable() { return Err(Error::argument("samples not writable")); } let mut normalizer = engine::normalize::Normalizer::new(self.sample_rate, &self.channels); @@ -48,9 +47,9 @@ impl Normalizer { } let report = normalizer - .normalize_in_place(frames.as_mut_slice()) + .normalize_in_place(samples.as_mut_slice()) .map_err(Error::runtime)?; - frames.write_back_in_place()?; + samples.write_back_in_place()?; Ok(NormalizeReport { report }) } From 75e82154c3a5ed7e3319677e525a6629aa670873 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Mon, 17 Aug 2026 07:54:03 +0900 Subject: [PATCH 106/122] test(ruby): restore test for MemoryView --- bindings/ruby/test/test_ebur128_stream.rb | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/bindings/ruby/test/test_ebur128_stream.rb b/bindings/ruby/test/test_ebur128_stream.rb index fb71b5c..c1cd34e 100644 --- a/bindings/ruby/test/test_ebur128_stream.rb +++ b/bindings/ruby/test/test_ebur128_stream.rb @@ -42,15 +42,10 @@ class EBUR128StreamTest < Test::Unit::TestCase test "push_interleaved MemoryView" do analyzer = Analyzer.new(channels: [:left, :right], modes: [:all]) - # Assigning an narray to a variable causes segmentation fault: - # - # > ruby(354,0x1f9285d80) malloc: *** error for object 0x7c7aef0ba6980052: pointer being freed was not allocated - # > ruby(354,0x1f9285d80) malloc: *** set a breakpoint in malloc_error_break to debug - # - # valid_data = Numo::SFloat[1.0, 1.0, 2.0, 2.0, 3.0, 3.0] - # assert_nothing_raised do - # analyzer.push_interleaved valid_data - # end + valid_data = Numo::SFloat[1.0, 1.0, 2.0, 2.0, 3.0, 3.0] + assert_nothing_raised do + analyzer.push_interleaved valid_data + end assert_nothing_raised do analyzer.push_interleaved Numo::SFloat[1.0, 1.0, 2.0, 2.0, 3.0, 3.0] From f2ea5c47ead1acb65fe203a2a14eb49e99bec1dc Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Mon, 17 Aug 2026 07:54:58 +0900 Subject: [PATCH 107/122] chore(ruby): ignore documentation directroy --- bindings/ruby/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/bindings/ruby/.gitignore b/bindings/ruby/.gitignore index 022858c..596263f 100644 --- a/bindings/ruby/.gitignore +++ b/bindings/ruby/.gitignore @@ -4,6 +4,7 @@ /coverage/ /doc/ /pkg/ +/html/ /spec/reports/ /tmp/ *.bundle From 94a573c62dcdcb038479a64885349719f9274572 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Mon, 17 Aug 2026 08:00:24 +0900 Subject: [PATCH 108/122] doc(ruby): add classes to RBS signature file --- bindings/ruby/sig/ebur128_stream.rbs | 65 ++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/bindings/ruby/sig/ebur128_stream.rbs b/bindings/ruby/sig/ebur128_stream.rbs index 2381e34..959f18b 100644 --- a/bindings/ruby/sig/ebur128_stream.rbs +++ b/bindings/ruby/sig/ebur128_stream.rbs @@ -1,4 +1,69 @@ module EBUR128Stream VERSION: String # See the writing guide of rbs: https://github.com/ruby/rbs#guides + + class Analyzer + def self.new: ( + channels: Array[Symbol], + ?sample_rate: Integer, + ?modes: Array[Symbol], + ?expected_duration: Integer + ) -> instance + + def channels: -> Array[Symbol] + def sample_rate: -> Integer + def modes: -> Array[Symbol] + def expected_duration: -> Integer + + def push_interleaved: (Array[Integer]) -> void + def push_planar: (Array[Array[Integer]]) -> void + + def snapshot: -> Snapshot + def reset: -> void + def finalize: -> Report + end + + class Normalizer + def new: ( + sample_rate: Integer, + channels: Array[Symbol], + ?target_lufs: Integer, + ?true_peak_ceiling_dbtp: Integer + ) -> instance + + def normalize_in_place: (Array[Integer]) -> NormalizeReport + end + + class Snapshot + include Reportable + + def momentary_lufs: -> (Integer | nil) + def short_term_lufs: -> (Integer | nil) + def integrated_lufs: -> (Integer | nil) + def loudness_range_lu: -> (Integer | nil) + def true_peak_dbtp: -> (Integer | nil) + def programme_duration_seconds: -> Integer + end + + class Report + include Reportable + + def integrated_lufs: -> (Integer | nil) + def loudness_range_lu: -> (Integer | nil) + def true_peak_dbtp: -> (Integer | nil) + def momentary_max_lufs: -> (Integer | nil) + def short_term_max_lufs: -> (Integer | nil) + def programme_duration_seconds: -> Integer + end + + class NormalizeReport + include Reportable + + def measured_integrated_lufs: -> (Integer | nil) + def measured_true_peak_dbtp: -> (Integer | nil) + def target_lufs: -> Integer + def true_peak_ceiling_dbtp: -> (Integer | nil) + def applied_gain_db: -> (Integer | nil) + def limited_by_true_peak: -> bool + end end From 9a402ab7e5485386822f72fe9fd87fec3737ac67 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Tue, 18 Aug 2026 11:50:10 +0900 Subject: [PATCH 109/122] feat(ruby): accept more MemoryView format --- .../ruby/ext/ebur128_stream/src/samples.rs | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/bindings/ruby/ext/ebur128_stream/src/samples.rs b/bindings/ruby/ext/ebur128_stream/src/samples.rs index 26b9f07..08f1a9e 100644 --- a/bindings/ruby/ext/ebur128_stream/src/samples.rs +++ b/bindings/ruby/ext/ebur128_stream/src/samples.rs @@ -4,6 +4,20 @@ use crate::{ }; use magnus::{RArray, Ruby, TryConvert, Value, error::IntoError}; +fn is_acceptable_format(format: &str) -> bool { + match format.chars().next() { + Some('f') => true, + + #[cfg(target_endian = "little")] + Some('e') => true, + + #[cfg(target_endian = "big")] + Some('g') => true, + + _ => false, + } +} + pub(crate) enum InterleavedSamples { Array { obj: RArray, samples: Vec }, MemoryView { view: MemoryView }, @@ -66,26 +80,27 @@ impl InterleavedSamples { if let Ok(view) = view { if view.ndim() == 1 && let Some(format) = view.format() + && is_acceptable_format(format) { // TODO: Check format more strictly(size, other expression) - if format == "f" { - return Some(Self::MemoryView { view }); - } + return Some(Self::MemoryView { view }); } } let view = MemoryView::::get(val, Flags::format().any_contiguous()); if let Ok(view) = view { if view.ndim() == 1 && let Some(format) = view.format() - && format == "f" + && is_acceptable_format(format) { return Some(Self::MemoryView { view }); } } let view = MemoryView::::get(val, Flags::simple()); if let Ok(view) = view { - let format = view.format(); - if view.ndim() == 1 && format.is_some() && format.unwrap() == "f" { + if view.ndim() == 1 + && let Some(format) = view.format() + && is_acceptable_format(format) + { return Some(Self::MemoryView { view }); } } @@ -135,7 +150,7 @@ impl PlanarSamples { if let Ok(view) = view { if view.ndim() == 2 && let Some(format) = view.format() - && format == "f" + && is_acceptable_format(format) { return Some(Self::MemoryView { view }); } @@ -144,8 +159,8 @@ impl PlanarSamples { if let Ok(view) = view { if !view.is_readonly() && view.ndim() == 2 - && view.format().is_some() - && view.format().unwrap() == "f" + && let Some(format) = view.format() + && is_acceptable_format(format) { return Some(Self::MemoryView { view }); } From 063210a7df261d8b2113122e12c84b22b811b522 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Tue, 18 Aug 2026 12:14:18 +0900 Subject: [PATCH 110/122] refactor(ruby): Make consume_memory_view return MemoryView --- .../ruby/ext/ebur128_stream/src/samples.rs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/bindings/ruby/ext/ebur128_stream/src/samples.rs b/bindings/ruby/ext/ebur128_stream/src/samples.rs index 08f1a9e..fd56220 100644 --- a/bindings/ruby/ext/ebur128_stream/src/samples.rs +++ b/bindings/ruby/ext/ebur128_stream/src/samples.rs @@ -25,8 +25,8 @@ pub(crate) enum InterleavedSamples { impl TryConvert for InterleavedSamples { fn try_convert(val: Value) -> Result { - if let Some(memory_view) = Self::consume_memory_view(val) { - Ok(memory_view) + if let Some(view) = Self::consume_memory_view(val) { + Ok(Self::MemoryView { view }) } else if let Some(obj) = RArray::from_value(val) { Ok(Self::Array { obj, @@ -75,7 +75,7 @@ impl InterleavedSamples { Ok(()) } - fn consume_memory_view(val: Value) -> Option { + fn consume_memory_view(val: Value) -> Option> { let view = MemoryView::::get(val, Flags::writable().format().any_contiguous()); if let Ok(view) = view { if view.ndim() == 1 @@ -83,7 +83,7 @@ impl InterleavedSamples { && is_acceptable_format(format) { // TODO: Check format more strictly(size, other expression) - return Some(Self::MemoryView { view }); + return Some(view); } } let view = MemoryView::::get(val, Flags::format().any_contiguous()); @@ -92,7 +92,7 @@ impl InterleavedSamples { && let Some(format) = view.format() && is_acceptable_format(format) { - return Some(Self::MemoryView { view }); + return Some(view); } } let view = MemoryView::::get(val, Flags::simple()); @@ -101,7 +101,7 @@ impl InterleavedSamples { && let Some(format) = view.format() && is_acceptable_format(format) { - return Some(Self::MemoryView { view }); + return Some(view); } } None @@ -115,8 +115,8 @@ pub(crate) enum PlanarSamples { impl TryConvert for PlanarSamples { fn try_convert(val: Value) -> Result { - if let Some(memory_view) = Self::consume_memory_view(val) { - Ok(memory_view) + if let Some(view) = Self::consume_memory_view(val) { + Ok(Self::MemoryView { view }) } else if let Some(obj) = RArray::from_value(val) { Ok(Self::Array { samples: obj.to_vec()?, @@ -145,14 +145,14 @@ impl PlanarSamples { } } - fn consume_memory_view(val: Value) -> Option { + fn consume_memory_view(val: Value) -> Option> { let view = MemoryView::::get(val, Flags::writable().format().row_major()); if let Ok(view) = view { if view.ndim() == 2 && let Some(format) = view.format() && is_acceptable_format(format) { - return Some(Self::MemoryView { view }); + return Some(view); } } let view = MemoryView::::get(val, Flags::simple()); @@ -162,7 +162,7 @@ impl PlanarSamples { && let Some(format) = view.format() && is_acceptable_format(format) { - return Some(Self::MemoryView { view }); + return Some(view); } } None From ba14128d1da19535c796024e0a939d3b4cd6603a Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Tue, 18 Aug 2026 12:36:51 +0900 Subject: [PATCH 111/122] reafactor(ruby): extract WritableSamples --- .../ruby/ext/ebur128_stream/src/normalize.rs | 10 +-- .../ruby/ext/ebur128_stream/src/samples.rs | 70 +++++++++++++------ 2 files changed, 54 insertions(+), 26 deletions(-) diff --git a/bindings/ruby/ext/ebur128_stream/src/normalize.rs b/bindings/ruby/ext/ebur128_stream/src/normalize.rs index 89ebea0..1812093 100644 --- a/bindings/ruby/ext/ebur128_stream/src/normalize.rs +++ b/bindings/ruby/ext/ebur128_stream/src/normalize.rs @@ -1,4 +1,4 @@ -use crate::{Channels, error::Error, samples::InterleavedSamples}; +use crate::{Channels, error::Error, samples::WritableInterleavedSamples}; use ebur128_stream_rs as engine; use magnus::{ RModule, Ruby, Value, function, method, @@ -34,10 +34,10 @@ impl Normalizer { }) } - fn normalize_in_place(&self, mut samples: InterleavedSamples) -> Result { - if !samples.is_writable() { - return Err(Error::argument("samples not writable")); - } + fn normalize_in_place( + &self, + mut samples: WritableInterleavedSamples, + ) -> Result { let mut normalizer = engine::normalize::Normalizer::new(self.sample_rate, &self.channels); if let Some(target_lufs) = self.target_lufs { normalizer = normalizer.target_lufs(target_lufs); diff --git a/bindings/ruby/ext/ebur128_stream/src/samples.rs b/bindings/ruby/ext/ebur128_stream/src/samples.rs index fd56220..72f2629 100644 --- a/bindings/ruby/ext/ebur128_stream/src/samples.rs +++ b/bindings/ruby/ext/ebur128_stream/src/samples.rs @@ -19,7 +19,7 @@ fn is_acceptable_format(format: &str) -> bool { } pub(crate) enum InterleavedSamples { - Array { obj: RArray, samples: Vec }, + Array { samples: Vec }, MemoryView { view: MemoryView }, } @@ -29,7 +29,6 @@ impl TryConvert for InterleavedSamples { Ok(Self::MemoryView { view }) } else if let Some(obj) = RArray::from_value(val) { Ok(Self::Array { - obj, samples: obj.to_vec()?, }) } else { @@ -40,22 +39,59 @@ impl TryConvert for InterleavedSamples { } impl InterleavedSamples { - // Instead of this method, should add Writable/ReadableInterleavedSamples, - // or try_concert_mut()? - pub(crate) fn is_writable(&self) -> bool { + pub(crate) fn as_slice(&self) -> &[f32] { match self { - Self::Array { .. } => true, - Self::MemoryView { view } => !view.is_readonly(), + Self::Array { samples } => samples, + Self::MemoryView { view } => view.data(), } } - pub(crate) fn as_slice(&self) -> &[f32] { - match self { - Self::Array { obj: _, samples } => samples, - Self::MemoryView { view } => view.data(), + fn consume_memory_view(val: Value) -> Option> { + let view = MemoryView::::get(val, Flags::format().any_contiguous()); + if let Ok(view) = view { + if view.ndim() == 1 + && let Some(format) = view.format() + && is_acceptable_format(format) + { + // TODO: Check format more strictly(size, other expression) + return Some(view); + } + } + let view = MemoryView::::get(val, Flags::simple()); + if let Ok(view) = view { + if view.ndim() == 1 + && let Some(format) = view.format() + && is_acceptable_format(format) + { + return Some(view); + } } + None } +} +pub(crate) enum WritableInterleavedSamples { + Array { obj: RArray, samples: Vec }, + MemoryView { view: MemoryView }, +} + +impl TryConvert for WritableInterleavedSamples { + fn try_convert(val: Value) -> Result { + if let Some(view) = Self::consume_memory_view(val) { + Ok(Self::MemoryView { view }) + } else if let Some(obj) = RArray::from_value(val) { + Ok(Self::Array { + obj, + samples: obj.to_vec()?, + }) + } else { + Err(Error::argument(format!("unsupported samples type: {val}")) + .into_error(&Ruby::get_with(val))) + } + } +} + +impl WritableInterleavedSamples { pub(crate) fn as_mut_slice(&mut self) -> &mut [f32] { match self { Self::Array { obj: _, samples } => samples, @@ -86,18 +122,10 @@ impl InterleavedSamples { return Some(view); } } - let view = MemoryView::::get(val, Flags::format().any_contiguous()); - if let Ok(view) = view { - if view.ndim() == 1 - && let Some(format) = view.format() - && is_acceptable_format(format) - { - return Some(view); - } - } let view = MemoryView::::get(val, Flags::simple()); if let Ok(view) = view { if view.ndim() == 1 + && !view.is_readonly() && let Some(format) = view.format() && is_acceptable_format(format) { @@ -146,7 +174,7 @@ impl PlanarSamples { } fn consume_memory_view(val: Value) -> Option> { - let view = MemoryView::::get(val, Flags::writable().format().row_major()); + let view = MemoryView::::get(val, Flags::format().row_major()); if let Ok(view) = view { if view.ndim() == 2 && let Some(format) = view.format() From fe1203e1c6c5fcaabb99f0c8b6e4c54ae833494f Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Wed, 19 Aug 2026 09:11:23 +0900 Subject: [PATCH 112/122] fix(ruby): swap expected and actual --- bindings/ruby/test/test_normalizer.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/ruby/test/test_normalizer.rb b/bindings/ruby/test/test_normalizer.rb index 753fb81..68fa90f 100644 --- a/bindings/ruby/test/test_normalizer.rb +++ b/bindings/ruby/test/test_normalizer.rb @@ -43,7 +43,7 @@ def test_normalize_in_place_memory_view normalizer2.normalize_in_place(samples2) assert_equal samples2.length, samples_mv.length samples_mv.each_with_index do |sample, i| - assert_in_delta sample, samples2[i], 0.001, "at sample #{i}" + assert_in_delta samples2[i], sample, 0.001, "at sample #{i}" end end end From 247db60eadeadcd0d2e151d56ad1bea08ab5e36e Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Wed, 19 Aug 2026 11:15:37 +0900 Subject: [PATCH 113/122] docs(ruby): complete README --- bindings/ruby/README.md | 234 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 223 insertions(+), 11 deletions(-) diff --git a/bindings/ruby/README.md b/bindings/ruby/README.md index 3e13cc3..7808135 100644 --- a/bindings/ruby/README.md +++ b/bindings/ruby/README.md @@ -1,38 +1,233 @@ # EBUR128Stream -TODO: Delete this and the text below, and describe your gem - -Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/ebur128_stream`. To experiment with that code, run `bin/console` for an interactive prompt. +A Ruby binding for [ebur128-stream][rust-impl], a streaming, zero-allocation EBU R128 loudness measurement in pure Rust. ## Installation -TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org. - Install the gem and add to the application's Gemfile by executing: ```bash -bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG +bundle add ebur128_stream ``` If bundler is not being used to manage dependencies, install the gem by executing: ```bash -gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG +gem install ebur128_stream ``` ## Usage -TODO: Write usage instructions here +```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 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 `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. +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 install this gem onto your local machine, run `bundle exec rake install`. 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). +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/[USERNAME]/ebur128_stream. +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 @@ -42,3 +237,20 @@ Licensed under either of - 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 From b7f0731a4b7ba35cc58a1666d630eba1ca0a859f Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Wed, 19 Aug 2026 11:42:21 +0900 Subject: [PATCH 114/122] docs(ruby): add example scripts --- bindings/ruby/sample/analyze-microphone.rb | 94 +++++++++++++++++++++ bindings/ruby/sample/analyze-planar-data.rb | 38 +++++++++ bindings/ruby/sample/analyze-wavefile.rb | 77 +++++++++++++++++ bindings/ruby/sample/normalize.rb | 35 ++++++++ 4 files changed, 244 insertions(+) create mode 100644 bindings/ruby/sample/analyze-microphone.rb create mode 100644 bindings/ruby/sample/analyze-planar-data.rb create mode 100644 bindings/ruby/sample/analyze-wavefile.rb create mode 100644 bindings/ruby/sample/normalize.rb diff --git a/bindings/ruby/sample/analyze-microphone.rb b/bindings/ruby/sample/analyze-microphone.rb new file mode 100644 index 0000000..9b15733 --- /dev/null +++ b/bindings/ruby/sample/analyze-microphone.rb @@ -0,0 +1,94 @@ +require "ebur128_stream" +require "gstreamer" +require "numo/narray/alt" +require "ndav/numo/narray" +require "io/console" + +CHANNELS = [:left, :right] +RATE = 48_000 +WIDTH = $stderr.winsize[1] +LIMIT = -70 + +include NDAV::Converter + +def main(argv) + analyser = setup_ebur128_stream + setup_gstreamer do |sample| + # GStreamer's Gst::Sample is 2-D but EBUR128Stream requires 1-D. + # Reshapes it using Numo::NArray + samples = NumoNArray(sample) + samples.reshape!(*samples.shape.reduce(:*)) + + analyser.push_interleaved samples + analyser.snapshot => {momentary_lufs:} + next unless momentary_lufs + + len = (-LIMIT + momentary_lufs) * (-WIDTH / LIMIT) + volume = "|" * len + padding = " " * (WIDTH - len) + print "\r#{volume}#{padding}" + end +end + +def setup_ebur128_stream + EBUR128Stream::Analyzer.new(channels: CHANNELS, sample_rate: RATE, modes: [:momentary]) +end + +def setup_gstreamer + pipeline = Gst::Pipeline.new("ebur128-stream") + src = Gst::ElementFactory.make("autoaudiosrc", nil) + convert = Gst::ElementFactory.make("audioconvert", nil) + resample = Gst::ElementFactory.make("audioresample", nil) + sink = Gst::ElementFactory.make("appsink", nil) + + caps = Gst::Caps.new("audio/x-raw") + caps["format"] = "F32LE" # F32 doesn't work for macOS + caps["rate", :int] = RATE + caps["channels", :int] = CHANNELS.length + caps["layout"] = "interleaved" + sink.caps = caps + + sink.emit_signals = true + sink.signal_connect :new_sample do |_| + begin + yield sink.pull_sample + rescue => err + $stderr.puts err + end + Gst::FlowReturn::OK + end + + pipeline << src << convert << resample << sink + src >> convert >> resample >> sink + + loop = GLib::MainLoop.new + + bus = pipeline.bus + bus.add_watch do |bus, message| + case message.type + when Gst::MessageType::EOS + loop.quit + when Gst::MessageType::ERROR + error, debug = message.parse_error + $stderr.puts error + $stderr.puts debug + loop.quit + end + true + end + + pipeline.play + begin + loop.run + rescue Interrupt + pp :Interrupt + rescue err + $stderr.puts err + ensure + pipeline.stop + GC.start + end + +end + +main ARGV diff --git a/bindings/ruby/sample/analyze-planar-data.rb b/bindings/ruby/sample/analyze-planar-data.rb new file mode 100644 index 0000000..eb723ca --- /dev/null +++ b/bindings/ruby/sample/analyze-planar-data.rb @@ -0,0 +1,38 @@ +require "ebur128_stream" +require "torchaudio" +require "ndav/torch/tensor" + +def main(argv) + waveform, sample_rate = TorchAudio.load(argv.shift) + + # TorchAudio.load returns planar waveform: + # [[L1, L2, L3, ...], [R1, R2, R3, ...]] + # which is suitable for push_panar + analyzer = EBUR128Stream::Analyzer.new(sample_rate:, channels: [:left, :right]) + analyzer.push_planar waveform + report = analyzer.finalize + + puts format_report(report) +end + +def format_report(report) + template = <<~EOS + === Report === + duration: %.2f seconds + integrated: %.2f LUFS + LRA: %.2f LU + true peak: %.2f dBTP + momentary max: %.2f LUFS + short term max: %.2f LUFS + EOS + template % { + dur: report.programme_duration_seconds, + int: report.integrated_lufs, + lra: report.loudness_range_lu, + tp: report.true_peak_dbtp, + mom: report.momentary_max_lufs, + st: report.short_term_max_lufs, + } +end + +main ARGV diff --git a/bindings/ruby/sample/analyze-wavefile.rb b/bindings/ruby/sample/analyze-wavefile.rb new file mode 100644 index 0000000..1644807 --- /dev/null +++ b/bindings/ruby/sample/analyze-wavefile.rb @@ -0,0 +1,77 @@ +require "ebur128_stream" +require "wavefile" +require "pathname" +require "tempfile" + +SAMPLE_RATE = 48_000 +FORMAT = WaveFile::Format.new(:stereo, :float, SAMPLE_RATE) + +def main(argv) + audio_path = argv.shift || make_fixture_audio + + analyzer = EBUR128Stream::Analyzer.new(channels: [:left, :right], sample_rate: SAMPLE_RATE) + WaveFile::Reader.new(audio_path).each_buffer do |buffer| + # WaveFile::Buffer#samples returns 2-D array: + # [[L1, R2], [L2, R2], [L3, R3], ...] + # EBUR128Stream::Analyzer#push_interleaved requires flat array: + # [L1, R1, L2, R2, L3, R3, ...] + analyzer.push_interleaved buffer.samples.flatten + snapshot = analyzer.snapshot + + puts format_snapshot(snapshot) + end + report = analyzer.finalize + + puts + puts format_report(report) +end + +def format_snapshot(snapshot) + template = "[%f] momentary: %{mom}, short term: %{st}, integrated: %{int}, true peak: %{tp}" + template % { + dur: snapshot.programme_duration_seconds, + mom: (snapshot.momentary_lufs&.to_s || "N/A")[..3], + st: (snapshot.short_term_lufs&.to_s || "N/A")[..3], + int: (snapshot.integrated_lufs&.to_s || "N/A")[..3], + tp: (snapshot.true_peak_dbtp&.to_s || "N/A")[..3], + } +end + +def format_report(report) + template = <<~EOS + === Report === + duration: %.2f seconds + integrated: %.2f LUFS + LRA: %.2f LU + true peak: %.2f dBTP + momentary max: %.2f LUFS + short term max: %.2f LUFS + EOS + template % { + dur: report.programme_duration_seconds, + int: report.integrated_lufs, + lra: report.loudness_range_lu, + tp: report.true_peak_dbtp, + mom: report.momentary_max_lufs, + st: report.short_term_max_lufs, + } +end + +def make_fixture_audio + file = Tempfile.new(["", ".wav"]) + + freq = 440 + length = 5 # seconds + WaveFile::Writer.new file.to_path, FORMAT do |writer| + (SAMPLE_RATE * length).times do |n| + phase = 2 * Math::PI * n / (SAMPLE_RATE / freq) + value = Math.sin(phase) + buffer = WaveFile::Buffer.new([[value, value]], FORMAT) # left, right + writer.write buffer + end + end + + file.to_path +end + +main ARGV diff --git a/bindings/ruby/sample/normalize.rb b/bindings/ruby/sample/normalize.rb new file mode 100644 index 0000000..0200a73 --- /dev/null +++ b/bindings/ruby/sample/normalize.rb @@ -0,0 +1,35 @@ +require "ebur128_stream" +require "torchaudio" +require "ndav/torch/tensor" + +def main(argv) + input = argv.shift + output = argv.shift + unless output + abort "Usage: ruby #{$PROGRAM_NAME} INPUT OUTPUT" + end + + waveform, sample_rate = TorchAudio.load(input) + + # TorchAudio returns a planar samples + # but, EBUR128Stream requires an interleaved samples for normalization. + # We need to reshape the waveform. + samples = waveform.transpose(1, 0) + shape = samples.shape + + # Currently 2-D array. Reshapes to 1-D. + samples = samples.reshape(shape.reduce(&:*)) + + normalizer = EBUR128Stream::Normalizer.new( + sample_rate:, + channels: [:left, :right], + target_lufs: -14.0 + ) + normalize_report = normalizer.normalize_in_place(samples) + + # Restores the samples to planar layout + out_samples = samples.reshape(shape).transpose(1, 0) + TorchAudio.save(output, samples, sample_rate) +end + +main ARGV From ff28bd5f84fcfb69767ae2bab2e30627053911fd Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Wed, 19 Aug 2026 14:09:30 +0900 Subject: [PATCH 115/122] test(ruby): extract TestAnalyzer --- bindings/ruby/test/test_analizer.rb | 114 ++++++++++++++++++++++ bindings/ruby/test/test_ebur128_stream.rb | 109 --------------------- 2 files changed, 114 insertions(+), 109 deletions(-) create mode 100644 bindings/ruby/test/test_analizer.rb diff --git a/bindings/ruby/test/test_analizer.rb b/bindings/ruby/test/test_analizer.rb new file mode 100644 index 0000000..e2cf67b --- /dev/null +++ b/bindings/ruby/test/test_analizer.rb @@ -0,0 +1,114 @@ +require_relative "helper" + +class TestAnalyzer < Test::Unit::TestCase + include EBUR128Stream + + test "new" do + assert_raise ArgumentError do + Analyzer.new + end + assert_instance_of Analyzer, Analyzer.new(channels: [:left]) + assert_raise ArgumentError do + Analyzer.new(channels: [:left, :nothing]) + end + assert_instance_of Analyzer, Analyzer.new(channels: [:center], sample_rate: 48_000) + assert_raise RuntimeError do + Analyzer.new(channels: [:center], modes: []) + end + assert_instance_of Analyzer, Analyzer.new(channels: [:center], modes: [:integrated, :true_peak]) + assert_raise ArgumentError do + Analyzer.new(channels: [:center], modes: [:unknown]) + end + end + + test "push_interleaved" do + analyzer = Analyzer.new(channels: [:left, :right], modes: [:all]) + assert_nothing_raised do + analyzer.push_interleaved [1.0, 1.0, 2.0, 2.0, 3.0, 3.0] + end + assert_raise ArgumentError do + analyzer.push_interleaved [1.0, 1.0, 2.0] + end + end + + test "push_interleaved MemoryView" do + analyzer = Analyzer.new(channels: [:left, :right], modes: [:all]) + + valid_data = Numo::SFloat[1.0, 1.0, 2.0, 2.0, 3.0, 3.0] + assert_nothing_raised do + analyzer.push_interleaved valid_data + end + + assert_nothing_raised do + analyzer.push_interleaved Numo::SFloat[1.0, 1.0, 2.0, 2.0, 3.0, 3.0] + end + assert_raise ArgumentError do + analyzer.push_interleaved Numo::SFloat[1.0, 1.0, 2.0] + end + end + + test "push_planar" do + analyzer = Analyzer.new(channels: [:left, :right], modes: [:all]) + assert_nothing_raised do + analyzer.push_planar [[1.0, 2.0, 3.0], [1.0, 2.0, 3.0]] + analyzer.push_planar [[1.0, 2.0, 3.0], [1.0, 2.0, 3.0]] + end + assert_raise ArgumentError do + analyzer.push_planar [[1.0, 2.0, 3.0], [1.0, 2.0, 3.0], [1.0, 2.0, 3.0]] + end + end + + test "push_planar MemoryView" do + analyzer = Analyzer.new(channels: [:left, :right], modes: [:all]) + assert_nothing_raised do + analyzer.push_planar Numo::SFloat[[1.0, 2.0, 3.0], [1.0, 2.0, 3.0]] + analyzer.push_planar Numo::SFloat[[1.0, 2.0, 3.0], [1.0, 2.0, 3.0]] + end + assert_raise ArgumentError do + analyzer.push_planar Numo::SFloat[[1.0, 2.0, 3.0], [1.0, 2.0, 3.0], [1.0, 2.0, 3.0]] + end + end + + test "finalize" do + analyzer = Analyzer.new(channels: [:left, :right], modes: [:all]) + analyzer.push_interleaved [1.0, 1.0, 2.0, 2.0, 3.0, 3.0] + report = nil + assert_nothing_raised do + report = analyzer.finalize + end + assert_instance_of Report, report + assert_raise_with_message RuntimeError, /finalized/ do + analyzer.finalize + end + end + + test "reset" do + analyzer = Analyzer.new(channels: [:left, :right], modes: [:all]) + analyzer.push_interleaved [1.0] * 48_000 * 2 + + assert_equal 1.0, analyzer.snapshot.programme_duration_seconds + analyzer.reset + assert_equal 0.0, analyzer.snapshot.programme_duration_seconds + end + + test "modes" do + analyzer = Analyzer.new(channels: [:left, :right], modes: [:momentary, :integrated]) + + assert_equal [:integrated, :momentary], analyzer.modes + end + + test "push_interleaved Array and MemoryView" do + samples = generate_samples + + analyzer_ary = Analyzer.new(channels: [:left, :right], modes: [:all]) + analyzer_ary.push_interleaved samples + report_ary = analyzer_ary.finalize + + samples_mv = Numo::SFloat.cast(samples) + analyzer_mv = Analyzer.new(channels: [:left, :right], modes: [:all]) + analyzer_mv.push_interleaved samples_mv + report_mv = analyzer_mv.finalize + + assert_equal report_ary, report_mv + end +end diff --git a/bindings/ruby/test/test_ebur128_stream.rb b/bindings/ruby/test/test_ebur128_stream.rb index c1cd34e..9df8bee 100644 --- a/bindings/ruby/test/test_ebur128_stream.rb +++ b/bindings/ruby/test/test_ebur128_stream.rb @@ -10,113 +10,4 @@ class EBUR128StreamTest < Test::Unit::TestCase ::EBUR128Stream.const_defined?(:VERSION) end end - - test "new" do - assert_raise ArgumentError do - Analyzer.new - end - assert_instance_of Analyzer, Analyzer.new(channels: [:left]) - assert_raise ArgumentError do - Analyzer.new(channels: [:left, :nothing]) - end - assert_instance_of Analyzer, Analyzer.new(channels: [:center], sample_rate: 48_000) - assert_raise RuntimeError do - Analyzer.new(channels: [:center], modes: []) - end - assert_instance_of Analyzer, Analyzer.new(channels: [:center], modes: [:integrated, :true_peak]) - assert_raise ArgumentError do - Analyzer.new(channels: [:center], modes: [:unknown]) - end - end - - test "push_interleaved" do - analyzer = Analyzer.new(channels: [:left, :right], modes: [:all]) - assert_nothing_raised do - analyzer.push_interleaved [1.0, 1.0, 2.0, 2.0, 3.0, 3.0] - end - assert_raise ArgumentError do - analyzer.push_interleaved [1.0, 1.0, 2.0] - end - end - - test "push_interleaved MemoryView" do - analyzer = Analyzer.new(channels: [:left, :right], modes: [:all]) - - valid_data = Numo::SFloat[1.0, 1.0, 2.0, 2.0, 3.0, 3.0] - assert_nothing_raised do - analyzer.push_interleaved valid_data - end - - assert_nothing_raised do - analyzer.push_interleaved Numo::SFloat[1.0, 1.0, 2.0, 2.0, 3.0, 3.0] - end - assert_raise ArgumentError do - analyzer.push_interleaved Numo::SFloat[1.0, 1.0, 2.0] - end - end - - test "push_planar" do - analyzer = Analyzer.new(channels: [:left, :right], modes: [:all]) - assert_nothing_raised do - analyzer.push_planar [[1.0, 2.0, 3.0], [1.0, 2.0, 3.0]] - analyzer.push_planar [[1.0, 2.0, 3.0], [1.0, 2.0, 3.0]] - end - assert_raise ArgumentError do - analyzer.push_planar [[1.0, 2.0, 3.0], [1.0, 2.0, 3.0], [1.0, 2.0, 3.0]] - end - end - - test "push_planar MemoryView" do - analyzer = Analyzer.new(channels: [:left, :right], modes: [:all]) - assert_nothing_raised do - analyzer.push_planar Numo::SFloat[[1.0, 2.0, 3.0], [1.0, 2.0, 3.0]] - analyzer.push_planar Numo::SFloat[[1.0, 2.0, 3.0], [1.0, 2.0, 3.0]] - end - assert_raise ArgumentError do - analyzer.push_planar Numo::SFloat[[1.0, 2.0, 3.0], [1.0, 2.0, 3.0], [1.0, 2.0, 3.0]] - end - end - - test "finalize" do - analyzer = Analyzer.new(channels: [:left, :right], modes: [:all]) - analyzer.push_interleaved [1.0, 1.0, 2.0, 2.0, 3.0, 3.0] - report = nil - assert_nothing_raised do - report = analyzer.finalize - end - assert_instance_of Report, report - assert_raise_with_message RuntimeError, /finalized/ do - analyzer.finalize - end - end - - test "reset" do - analyzer = Analyzer.new(channels: [:left, :right], modes: [:all]) - analyzer.push_interleaved [1.0] * 48_000 * 2 - - assert_equal 1.0, analyzer.snapshot.programme_duration_seconds - analyzer.reset - assert_equal 0.0, analyzer.snapshot.programme_duration_seconds - end - - test "modes" do - analyzer = Analyzer.new(channels: [:left, :right], modes: [:momentary, :integrated]) - - assert_equal [:integrated, :momentary], analyzer.modes - end - - test "push_interleaved Array and MemoryView" do - samples = generate_samples - - analyzer_ary = Analyzer.new(channels: [:left, :right], modes: [:all]) - analyzer_ary.push_interleaved samples - report_ary = analyzer_ary.finalize - - samples_mv = Numo::SFloat.cast(samples) - analyzer_mv = Analyzer.new(channels: [:left, :right], modes: [:all]) - analyzer_mv.push_interleaved samples_mv - report_mv = analyzer_mv.finalize - - assert_equal report_ary, report_mv - end end From 88b70f2230677a1130d0dfe0319eaaa1029c1098 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Thu, 20 Aug 2026 11:53:36 +0900 Subject: [PATCH 116/122] feat(ruby): add ItemDesc feature to MemoryView --- .../ruby/ext/ebur128_stream/src/analyzer.rs | 3 +- .../ext/ebur128_stream/src/memory_view.rs | 90 ++++++++++++++++++- 2 files changed, 89 insertions(+), 4 deletions(-) diff --git a/bindings/ruby/ext/ebur128_stream/src/analyzer.rs b/bindings/ruby/ext/ebur128_stream/src/analyzer.rs index 2f7eab1..04980fa 100644 --- a/bindings/ruby/ext/ebur128_stream/src/analyzer.rs +++ b/bindings/ruby/ext/ebur128_stream/src/analyzer.rs @@ -91,7 +91,8 @@ impl Analyzer { } fn push_planar(&self, samples: PlanarSamples) -> Result<(), Error> { - Ok(self.analyzer_mut()? + Ok(self + .analyzer_mut()? .push_planar(&samples.channel_slices())?) } diff --git a/bindings/ruby/ext/ebur128_stream/src/memory_view.rs b/bindings/ruby/ext/ebur128_stream/src/memory_view.rs index 237fa85..5d15d0b 100644 --- a/bindings/ruby/ext/ebur128_stream/src/memory_view.rs +++ b/bindings/ruby/ext/ebur128_stream/src/memory_view.rs @@ -1,10 +1,10 @@ use magnus::{ Error, Ruby, Value, - rb_sys::{AsRawValue, protect}, + rb_sys::{AsRawValue, FromRawValue, protect}, }; use rb_sys::{ - Qnil, rb_memory_view_get, rb_memory_view_parse_item_format, rb_memory_view_release, - rb_memory_view_t, + Qnil, rb_memory_view_get, rb_memory_view_item_component_t, rb_memory_view_parse_item_format, + rb_memory_view_prepare_item_desc, rb_memory_view_release, rb_memory_view_t, ruby_memory_view_flags::{ RUBY_MEMORY_VIEW_ANY_CONTIGUOUS, RUBY_MEMORY_VIEW_COLUMN_MAJOR, RUBY_MEMORY_VIEW_FORMAT, RUBY_MEMORY_VIEW_INDIRECT, RUBY_MEMORY_VIEW_MULTI_DIMENSIONAL, RUBY_MEMORY_VIEW_ROW_MAJOR, @@ -215,6 +215,10 @@ impl MemoryView { Ok(view) } + pub fn obj(&self) -> Value { + unsafe { Value::from_raw(self.inner.obj) } + } + pub fn ndim(&self) -> usize { usize::try_from(self.inner.ndim).expect("ndim validated in get()") } @@ -264,6 +268,23 @@ impl MemoryView { unsafe { slice::from_raw_parts_mut(ptr, n_items) } } + fn prepare_item_desc(&mut self) -> Result<(), magnus::Error> { + protect(|| { + unsafe { + rb_memory_view_prepare_item_desc(&mut self.inner as *mut rb_memory_view_t); + } + Qnil.into() + })?; + + Ok(()) + } + + pub fn item_desc(&mut self) -> Result { + let _ = self.prepare_item_desc(); + let view: &Self = self; + ItemDesc::try_from(view) + } + // Just for validation and retrieving item size // TODO: Implement parse_item_format() properly fn validate_format(ruby: &Ruby, format: &CStr) -> Result { @@ -297,3 +318,66 @@ impl MemoryView { } } } + +pub struct ItemDesc { + length: usize, + components: Vec, +} + +impl TryFrom<&MemoryView> for ItemDesc { + type Error = Error; + + fn try_from(value: &MemoryView) -> Result { + let length = usize::try_from(value.inner.item_desc.length).map_err(|_| { + let ruby = Ruby::get_with(value.obj()); + Error::new(ruby.exception_arg_error(), "invalid item_desc.length") + })?; + let components = value.inner.item_desc.components; + if (length == 0 && !components.is_null()) || (length > 0 && components.is_null()) { + let ruby = Ruby::get_with(value.obj()); + return Err(Error::new(ruby.exception_arg_error(), "invalid item_desc")); + } + let components = unsafe { slice::from_raw_parts(components, length) }; + Ok(Self { + length, + components: components.iter().map(ItemComponent::from).collect(), + }) + } +} + +impl IntoIterator for ItemDesc { + type Item = ItemComponent; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.components.into_iter() + } +} + +impl ItemDesc { + pub fn len(&self) -> usize { + self.length + } +} + +pub struct ItemComponent { + pub format: char, + pub is_native_size: bool, + pub is_little_endian: bool, + pub offset: usize, + pub size: usize, + pub repeat: usize, +} + +impl<'a> From<&'a rb_memory_view_item_component_t> for ItemComponent { + fn from(value: &'a rb_memory_view_item_component_t) -> Self { + Self { + format: char::from(value.format as u8), + is_native_size: value.native_size_p, + is_little_endian: value.little_endian_p, + offset: value.offset as usize, + size: value.size as usize, + repeat: value.repeat as usize, + } + } +} From ce1d336e777387eaceceebf1d23c02f7db135bc1 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Thu, 20 Aug 2026 13:10:20 +0900 Subject: [PATCH 117/122] feat(ruby): check MemoryView more strictly --- .../ext/ebur128_stream/src/memory_view.rs | 2 +- .../ruby/ext/ebur128_stream/src/samples.rs | 103 ++++++++++-------- 2 files changed, 60 insertions(+), 45 deletions(-) diff --git a/bindings/ruby/ext/ebur128_stream/src/memory_view.rs b/bindings/ruby/ext/ebur128_stream/src/memory_view.rs index 5d15d0b..0d31f74 100644 --- a/bindings/ruby/ext/ebur128_stream/src/memory_view.rs +++ b/bindings/ruby/ext/ebur128_stream/src/memory_view.rs @@ -280,7 +280,7 @@ impl MemoryView { } pub fn item_desc(&mut self) -> Result { - let _ = self.prepare_item_desc(); + self.prepare_item_desc()?; let view: &Self = self; ItemDesc::try_from(view) } diff --git a/bindings/ruby/ext/ebur128_stream/src/samples.rs b/bindings/ruby/ext/ebur128_stream/src/samples.rs index 72f2629..cae4422 100644 --- a/bindings/ruby/ext/ebur128_stream/src/samples.rs +++ b/bindings/ruby/ext/ebur128_stream/src/samples.rs @@ -1,20 +1,26 @@ use crate::{ error::Error, - memory_view::{Flags, FlagsChainable, MemoryView}, + memory_view::{Flags, FlagsChainable, ItemComponent, MemoryView}, }; use magnus::{RArray, Ruby, TryConvert, Value, error::IntoError}; -fn is_acceptable_format(format: &str) -> bool { - match format.chars().next() { - Some('f') => true, +fn is_acceptable_component(component: ItemComponent) -> bool { + component.offset == 0 + && component.repeat == 1 + && is_acceptable_format(component.format) +} + +fn is_acceptable_format(format: char) -> bool { + match format { + 'f' => true, #[cfg(target_endian = "little")] - Some('e') => true, + 'e' => true, #[cfg(target_endian = "big")] - Some('g') => true, + 'g' => true, - _ => false, + _ => false } } @@ -47,27 +53,31 @@ impl InterleavedSamples { } fn consume_memory_view(val: Value) -> Option> { - let view = MemoryView::::get(val, Flags::format().any_contiguous()); - if let Ok(view) = view { - if view.ndim() == 1 - && let Some(format) = view.format() - && is_acceptable_format(format) - { - // TODO: Check format more strictly(size, other expression) + let view = MemoryView::::get(val, Flags::any_contiguous()); + if let Ok(mut view) = view { + if Self::is_acceptable(&mut view).unwrap_or(false) { return Some(view); } } let view = MemoryView::::get(val, Flags::simple()); - if let Ok(view) = view { - if view.ndim() == 1 - && let Some(format) = view.format() - && is_acceptable_format(format) - { + if let Ok(mut view) = view { + if Self::is_acceptable(&mut view).unwrap_or(false) { return Some(view); } } None } + + // TODO: Check format more strictly(size, other expression) + fn is_acceptable(view: &mut MemoryView) -> Result { + let item_desc = view.item_desc()?; + Ok(view.ndim() == 1 + && item_desc.len() == 1 + && item_desc + .into_iter() + .next() + .is_some_and(is_acceptable_component)) + } } pub(crate) enum WritableInterleavedSamples { @@ -112,28 +122,30 @@ impl WritableInterleavedSamples { } fn consume_memory_view(val: Value) -> Option> { - let view = MemoryView::::get(val, Flags::writable().format().any_contiguous()); - if let Ok(view) = view { - if view.ndim() == 1 - && let Some(format) = view.format() - && is_acceptable_format(format) - { - // TODO: Check format more strictly(size, other expression) + let view = MemoryView::::get(val, Flags::writable().any_contiguous()); + if let Ok(mut view) = view { + if Self::is_acceptable(&mut view).unwrap_or(false) { return Some(view); } } let view = MemoryView::::get(val, Flags::simple()); - if let Ok(view) = view { - if view.ndim() == 1 - && !view.is_readonly() - && let Some(format) = view.format() - && is_acceptable_format(format) - { + if let Ok(mut view) = view { + if !view.is_readonly() && Self::is_acceptable(&mut view).unwrap_or(false) { return Some(view); } } None } + + fn is_acceptable(view: &mut MemoryView) -> Result { + let item_desc = view.item_desc()?; + Ok(view.ndim() == 1 + && item_desc.len() == 1 + && item_desc + .into_iter() + .next() + .is_some_and(is_acceptable_component)) + } } pub(crate) enum PlanarSamples { @@ -174,25 +186,28 @@ impl PlanarSamples { } fn consume_memory_view(val: Value) -> Option> { - let view = MemoryView::::get(val, Flags::format().row_major()); - if let Ok(view) = view { - if view.ndim() == 2 - && let Some(format) = view.format() - && is_acceptable_format(format) - { + let view = MemoryView::::get(val, Flags::row_major()); + if let Ok(mut view) = view { + if Self::is_acceptable(&mut view).unwrap_or(false) { return Some(view); } } let view = MemoryView::::get(val, Flags::simple()); - if let Ok(view) = view { - if !view.is_readonly() - && view.ndim() == 2 - && let Some(format) = view.format() - && is_acceptable_format(format) - { + if let Ok(mut view) = view { + if Self::is_acceptable(&mut view).unwrap_or(false) { return Some(view); } } None } + + fn is_acceptable(view: &mut MemoryView) -> Result { + let item_desc = view.item_desc()?; + Ok(view.ndim() == 2 + && item_desc.len() == 1 + && item_desc + .into_iter() + .next() + .is_some_and(is_acceptable_component)) + } } From 24735982b54a43897c39e9c7555b4deb30a954e0 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Thu, 20 Aug 2026 13:11:22 +0900 Subject: [PATCH 118/122] refactor(ruby): remove unnecessary lifetime --- bindings/ruby/ext/ebur128_stream/src/analyzer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/ruby/ext/ebur128_stream/src/analyzer.rs b/bindings/ruby/ext/ebur128_stream/src/analyzer.rs index 04980fa..67bac25 100644 --- a/bindings/ruby/ext/ebur128_stream/src/analyzer.rs +++ b/bindings/ruby/ext/ebur128_stream/src/analyzer.rs @@ -72,7 +72,7 @@ impl Analyzer { Ok(Channels::from(rb_self.analyzer()?.channels()).try_into_rarray(ruby)?) } - fn modes<'a>(ruby: &Ruby, rb_self: &'a Self) -> Result { + fn modes(ruby: &Ruby, rb_self: &Self) -> Result { let syms = rb_self .analyzer()? .modes() From f1cc86e5bca6e8643e0d0b8b72c5aa9d524a1b0f Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Thu, 20 Aug 2026 18:21:16 +0900 Subject: [PATCH 119/122] chore(ruby): update microphone example --- bindings/ruby/sample/analyze-microphone.rb | 26 +++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/bindings/ruby/sample/analyze-microphone.rb b/bindings/ruby/sample/analyze-microphone.rb index 9b15733..40eb3c3 100644 --- a/bindings/ruby/sample/analyze-microphone.rb +++ b/bindings/ruby/sample/analyze-microphone.rb @@ -6,13 +6,20 @@ CHANNELS = [:left, :right] RATE = 48_000 -WIDTH = $stderr.winsize[1] +HEIGHT = $stdout.winsize[0] +WIDTH = $stdout.winsize[1] +PADDING_BLOCK_START = HEIGHT / 2 - 2 +PADDING_INLINE = WIDTH / 5 +AREA_WIDTH = WIDTH - PADDING_INLINE * 2 LIMIT = -70 include NDAV::Converter def main(argv) analyser = setup_ebur128_stream + + print "\e[2J" + setup_gstreamer do |sample| # GStreamer's Gst::Sample is 2-D but EBUR128Stream requires 1-D. # Reshapes it using Numo::NArray @@ -23,10 +30,7 @@ def main(argv) analyser.snapshot => {momentary_lufs:} next unless momentary_lufs - len = (-LIMIT + momentary_lufs) * (-WIDTH / LIMIT) - volume = "|" * len - padding = " " * (WIDTH - len) - print "\r#{volume}#{padding}" + render_loudness momentary_lufs end end @@ -88,7 +92,19 @@ def setup_gstreamer pipeline.stop GC.start end +end +def render_loudness(loudness) + len = (-LIMIT + loudness) * (-AREA_WIDTH / LIMIT) + volume = "|" * len + ws = " " * (AREA_WIDTH - len) + print "\e[#{PADDING_BLOCK_START};#{PADDING_INLINE}H" + print "\e[#{PADDING_INLINE}G" + puts "#{volume}#{ws}" + puts + digits = "%.3f" % loudness + print "\e[#{WIDTH - PADDING_INLINE - digits.to_s.length}G" + print digits end main ARGV From bca903236b11254241749e77c06e6a51ffa310dc Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Fri, 21 Aug 2026 12:00:35 +0900 Subject: [PATCH 120/122] refactor(ruby): remove needless method calls --- bindings/ruby/ext/ebur128_stream/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/ruby/ext/ebur128_stream/src/lib.rs b/bindings/ruby/ext/ebur128_stream/src/lib.rs index 267f9a3..326fc2a 100644 --- a/bindings/ruby/ext/ebur128_stream/src/lib.rs +++ b/bindings/ruby/ext/ebur128_stream/src/lib.rs @@ -61,7 +61,7 @@ impl Deref for Channels { impl<'a> From<&'a [engine::Channel]> for Channels { fn from(value: &'a [engine::Channel]) -> Self { Self { - inner: value.to_vec().into_iter().collect(), + inner: value.to_vec() } } } From b9a79c5bf9bb3b314fcae7b292adf10ab1aa3df5 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Fri, 21 Aug 2026 12:09:46 +0900 Subject: [PATCH 121/122] doc(ruby): fix a typo --- bindings/ruby/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/ruby/README.md b/bindings/ruby/README.md index 7808135..778b21a 100644 --- a/bindings/ruby/README.md +++ b/bindings/ruby/README.md @@ -122,7 +122,7 @@ 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 was not specified at initialization. +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`: From e432948589cb1b15539d55bf8fec2bcdd9f877d8 Mon Sep 17 00:00:00 2001 From: Kitaiti Makoto Date: Tue, 1 Sep 2026 23:31:48 +0900 Subject: [PATCH 122/122] refactor(ruby): use grey_knights::memory_view::ValidatedMemoryView instead of local MemoryView --- bindings/ruby/ext/ebur128_stream/Cargo.lock | 22 +- bindings/ruby/ext/ebur128_stream/Cargo.toml | 1 + bindings/ruby/ext/ebur128_stream/src/lib.rs | 3 +- .../ext/ebur128_stream/src/memory_view.rs | 383 ------------------ .../ruby/ext/ebur128_stream/src/samples.rs | 42 +- 5 files changed, 37 insertions(+), 414 deletions(-) delete mode 100644 bindings/ruby/ext/ebur128_stream/src/memory_view.rs diff --git a/bindings/ruby/ext/ebur128_stream/Cargo.lock b/bindings/ruby/ext/ebur128_stream/Cargo.lock index 52642ff..b7b6b39 100644 --- a/bindings/ruby/ext/ebur128_stream/Cargo.lock +++ b/bindings/ruby/ext/ebur128_stream/Cargo.lock @@ -72,6 +72,7 @@ name = "ebur128-stream" version = "0.1.0" dependencies = [ "ebur128-stream 0.2.0", + "grey-knights", "magnus", "rb-sys", "rb-sys-env", @@ -91,9 +92,9 @@ dependencies = [ [[package]] name = "either" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" [[package]] name = "glob" @@ -101,6 +102,15 @@ 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 = "git+https://gitlab.com/KitaitiMakoto/grey-knights.git#3aaf7110bf7e39c617033b72e87eb1401ccc0e0b" +dependencies = [ + "magnus", + "rb-sys", +] + [[package]] name = "itertools" version = "0.13.0" @@ -203,18 +213,18 @@ dependencies = [ [[package]] name = "rb-sys" -version = "0.9.128" +version = "0.9.130" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45ca28513560e56cfb79a62b1fce363c73af170a182024ce880c77ee9429920a" +checksum = "02faf625bb10ba893e3ae620f19c9fb1b5f8fcae0fe4eb86bb3f2230fad75edb" dependencies = [ "rb-sys-build", ] [[package]] name = "rb-sys-build" -version = "0.9.128" +version = "0.9.130" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce04b2c55eff3a21aaa623fcc655d94373238e72cac6b3e1a3641ff31649f99a" +checksum = "05be6f9c86fe5482808162826f6c047d093b3670582296c770de1d94f8066694" dependencies = [ "bindgen", "lazy_static", diff --git a/bindings/ruby/ext/ebur128_stream/Cargo.toml b/bindings/ruby/ext/ebur128_stream/Cargo.toml index cb776cd..e974f8d 100644 --- a/bindings/ruby/ext/ebur128_stream/Cargo.toml +++ b/bindings/ruby/ext/ebur128_stream/Cargo.toml @@ -12,6 +12,7 @@ crate-type = ["cdylib"] 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 = { git = "https://gitlab.com/KitaitiMakoto/grey-knights.git" } [build-dependencies] rb-sys-env = "0.2.2" diff --git a/bindings/ruby/ext/ebur128_stream/src/lib.rs b/bindings/ruby/ext/ebur128_stream/src/lib.rs index 326fc2a..c4300ec 100644 --- a/bindings/ruby/ext/ebur128_stream/src/lib.rs +++ b/bindings/ruby/ext/ebur128_stream/src/lib.rs @@ -1,6 +1,5 @@ mod analyzer; mod error; -mod memory_view; mod normalize; mod report; mod samples; @@ -61,7 +60,7 @@ impl Deref for Channels { impl<'a> From<&'a [engine::Channel]> for Channels { fn from(value: &'a [engine::Channel]) -> Self { Self { - inner: value.to_vec() + inner: value.to_vec(), } } } diff --git a/bindings/ruby/ext/ebur128_stream/src/memory_view.rs b/bindings/ruby/ext/ebur128_stream/src/memory_view.rs deleted file mode 100644 index 0d31f74..0000000 --- a/bindings/ruby/ext/ebur128_stream/src/memory_view.rs +++ /dev/null @@ -1,383 +0,0 @@ -use magnus::{ - Error, Ruby, Value, - rb_sys::{AsRawValue, FromRawValue, protect}, -}; -use rb_sys::{ - Qnil, rb_memory_view_get, rb_memory_view_item_component_t, rb_memory_view_parse_item_format, - rb_memory_view_prepare_item_desc, rb_memory_view_release, rb_memory_view_t, - ruby_memory_view_flags::{ - RUBY_MEMORY_VIEW_ANY_CONTIGUOUS, RUBY_MEMORY_VIEW_COLUMN_MAJOR, RUBY_MEMORY_VIEW_FORMAT, - RUBY_MEMORY_VIEW_INDIRECT, RUBY_MEMORY_VIEW_MULTI_DIMENSIONAL, RUBY_MEMORY_VIEW_ROW_MAJOR, - RUBY_MEMORY_VIEW_SIMPLE, RUBY_MEMORY_VIEW_STRIDES, RUBY_MEMORY_VIEW_WRITABLE, - }, -}; -use std::{ffi::CStr, marker::PhantomData, mem::MaybeUninit, ptr, slice}; - -pub struct Flags(i32); - -impl From for i32 { - fn from(value: Flags) -> Self { - value.0 - } -} - -impl Flags { - pub fn simple() -> Self { - Self(RUBY_MEMORY_VIEW_SIMPLE as i32) - } - - pub fn writable() -> Self { - Self(RUBY_MEMORY_VIEW_WRITABLE as i32) - } - - pub fn format() -> Self { - Self(RUBY_MEMORY_VIEW_FORMAT as i32) - } - - pub fn multi_dimensional() -> Self { - Self(RUBY_MEMORY_VIEW_MULTI_DIMENSIONAL as i32) - } - - pub fn strides() -> Self { - Self(RUBY_MEMORY_VIEW_STRIDES as i32) - } - - pub fn row_major() -> Self { - Self(RUBY_MEMORY_VIEW_ROW_MAJOR as i32) - } - - pub fn column_major() -> Self { - Self(RUBY_MEMORY_VIEW_COLUMN_MAJOR as i32) - } - - pub fn any_contiguous() -> Self { - Self(RUBY_MEMORY_VIEW_ANY_CONTIGUOUS as i32) - } - - pub fn indirect() -> Self { - Self(RUBY_MEMORY_VIEW_INDIRECT as i32) - } -} - -pub trait FlagsChainable { - fn writable(self) -> Self; - fn format(self) -> Self; - fn multi_dimensional(self) -> Self; - fn strides(self) -> Self; - fn row_major(self) -> Self; - fn column_major(self) -> Self; - fn any_contiguous(self) -> Self; - fn indirect(self) -> Self; -} - -impl FlagsChainable for Flags { - fn writable(mut self) -> Self { - self.0 |= RUBY_MEMORY_VIEW_WRITABLE as i32; - self - } - - fn format(mut self) -> Self { - self.0 |= RUBY_MEMORY_VIEW_FORMAT as i32; - self - } - - fn multi_dimensional(mut self) -> Self { - self.0 |= RUBY_MEMORY_VIEW_MULTI_DIMENSIONAL as i32; - self - } - - fn strides(mut self) -> Self { - self.0 |= RUBY_MEMORY_VIEW_STRIDES as i32; - self - } - - fn row_major(mut self) -> Self { - self.0 |= RUBY_MEMORY_VIEW_ROW_MAJOR as i32; - self - } - - fn column_major(mut self) -> Self { - self.0 |= RUBY_MEMORY_VIEW_COLUMN_MAJOR as i32; - self - } - - fn any_contiguous(mut self) -> Self { - self.0 |= RUBY_MEMORY_VIEW_ANY_CONTIGUOUS as i32; - self - } - - fn indirect(mut self) -> Self { - self.0 |= RUBY_MEMORY_VIEW_INDIRECT as i32; - self - } -} - -pub struct MemoryView { - inner: rb_memory_view_t, - marker: PhantomData, -} - -impl Drop for MemoryView { - fn drop(&mut self) { - let _ = protect(|| { - // SAFETY: This guard is generated from successfully allocated rb_memory_view_t and drop() is called only once so it's not released more than once - unsafe { - rb_memory_view_release(&mut self.inner); - } - Qnil.into() - }); - } -} - -impl MemoryView { - pub fn get(obj: Value, flags: Flags) -> Result { - let ruby = Ruby::get_with(obj); - - let obj = obj.as_raw(); - let mut view = MaybeUninit::zeroed(); - - let mut result = false; - protect(|| { - result = unsafe { rb_memory_view_get(obj, view.as_mut_ptr(), flags.into()) }; - Qnil.into() - })?; - if !result { - return Err(Error::new( - ruby.exception_runtime_error(), - "MemoryView not got", - )); - } - - let view = unsafe { view.assume_init() }; - let view = Self { - inner: view, - marker: PhantomData, - }; - - // Validation - let ndim = usize::try_from(view.inner.ndim) - .map_err(|_| Error::new(ruby.exception_arg_error(), "invalid ndim"))?; - usize::try_from(view.inner.byte_size) - .map_err(|_| Error::new(ruby.exception_arg_error(), "invalid byte_size"))?; - let item_size = usize::try_from(view.inner.item_size) - .map_err(|_| Error::new(ruby.exception_arg_error(), "invalid item_size"))?; - - let format = view.inner.format; - if !format.is_null() { - let format = unsafe { CStr::from_ptr(format) }; - let item_size_by_format = Self::validate_format(&ruby, format)?; - if item_size_by_format != item_size { - Err(Error::new( - ruby.exception_arg_error(), - "item_size and item size calculated by format not match", - ))?; - } - } - - if view.inner.shape.is_null() { - if ndim > 1 { - Err(Error::new( - ruby.exception_arg_error(), - "ndim > 1 but shape is NULL", - ))?; - } - } else { - // SAFETY: rb_memory_view_t.shape is *ssize_t - let shape = unsafe { slice::from_raw_parts(view.inner.shape, ndim) }; - if !view.inner.shape.cast::().is_aligned() { - Err(Error::new( - ruby.exception_arg_error(), - "shape not aligned for usize", - ))?; - } - for &dim in shape { - usize::try_from(dim).map_err(|_| { - Error::new( - ruby.exception_arg_error(), - format!("dimension {dim} of shape invalid"), - ) - })?; - } - } - - let data = view.inner.data; - if data.is_null() { - Err(Error::new(ruby.exception_runtime_error(), "data is NULL"))?; - } - let ptr = data.cast::(); - if !ptr.is_aligned() { - Err(Error::new( - ruby.exception_runtime_error(), - "data not aligned", - ))?; - } - - Ok(view) - } - - pub fn obj(&self) -> Value { - unsafe { Value::from_raw(self.inner.obj) } - } - - pub fn ndim(&self) -> usize { - usize::try_from(self.inner.ndim).expect("ndim validated in get()") - } - - pub fn byte_size(&self) -> usize { - usize::try_from(self.inner.byte_size).expect("byte_size validated in get()") - } - - pub fn shape(&self) -> Option<&[usize]> { - if self.inner.shape.is_null() { - return None; - } - // SAFETY: validated in get() - Some(unsafe { slice::from_raw_parts(self.inner.shape.cast::(), self.ndim()) }) - } - - pub fn is_readonly(&self) -> bool { - self.inner.readonly - } - - pub fn format(&self) -> Option<&str> { - if self.inner.format.is_null() { - None - } else { - // SAFETY: format is valid because parse_item_format() is called in get() - Some( - unsafe { CStr::from_ptr(self.inner.format) } - .to_str() - .unwrap(), - ) - } - } - - pub fn data(&self) -> &[T] { - let n_items = self.byte_size() / size_of::(); - let data = self.inner.data; - let ptr = data.cast::(); - - unsafe { slice::from_raw_parts(ptr, n_items) } - } - - pub fn data_as_mut(&mut self) -> &mut [T] { - let n_items = self.byte_size() / size_of::(); - let data = self.inner.data; - let ptr = data.cast::(); - - unsafe { slice::from_raw_parts_mut(ptr, n_items) } - } - - fn prepare_item_desc(&mut self) -> Result<(), magnus::Error> { - protect(|| { - unsafe { - rb_memory_view_prepare_item_desc(&mut self.inner as *mut rb_memory_view_t); - } - Qnil.into() - })?; - - Ok(()) - } - - pub fn item_desc(&mut self) -> Result { - self.prepare_item_desc()?; - let view: &Self = self; - ItemDesc::try_from(view) - } - - // Just for validation and retrieving item size - // TODO: Implement parse_item_format() properly - fn validate_format(ruby: &Ruby, format: &CStr) -> Result { - let mut item_size = -1; - - protect(|| { - item_size = unsafe { - rb_memory_view_parse_item_format( - format.as_ptr(), - ptr::null_mut(), - ptr::null_mut(), - ptr::null_mut(), - ) - }; - Qnil.into() - })?; - - if item_size < 0 { - Err(Error::new( - ruby.exception_runtime_error(), - "failed to parse format", - )) - } else { - let item_size = usize::try_from(item_size).map_err(|_| { - Error::new( - ruby.exception_runtime_error(), - "format parse returned invalid item_size", - ) - })?; - Ok(item_size) - } - } -} - -pub struct ItemDesc { - length: usize, - components: Vec, -} - -impl TryFrom<&MemoryView> for ItemDesc { - type Error = Error; - - fn try_from(value: &MemoryView) -> Result { - let length = usize::try_from(value.inner.item_desc.length).map_err(|_| { - let ruby = Ruby::get_with(value.obj()); - Error::new(ruby.exception_arg_error(), "invalid item_desc.length") - })?; - let components = value.inner.item_desc.components; - if (length == 0 && !components.is_null()) || (length > 0 && components.is_null()) { - let ruby = Ruby::get_with(value.obj()); - return Err(Error::new(ruby.exception_arg_error(), "invalid item_desc")); - } - let components = unsafe { slice::from_raw_parts(components, length) }; - Ok(Self { - length, - components: components.iter().map(ItemComponent::from).collect(), - }) - } -} - -impl IntoIterator for ItemDesc { - type Item = ItemComponent; - type IntoIter = std::vec::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - self.components.into_iter() - } -} - -impl ItemDesc { - pub fn len(&self) -> usize { - self.length - } -} - -pub struct ItemComponent { - pub format: char, - pub is_native_size: bool, - pub is_little_endian: bool, - pub offset: usize, - pub size: usize, - pub repeat: usize, -} - -impl<'a> From<&'a rb_memory_view_item_component_t> for ItemComponent { - fn from(value: &'a rb_memory_view_item_component_t) -> Self { - Self { - format: char::from(value.format as u8), - is_native_size: value.native_size_p, - is_little_endian: value.little_endian_p, - offset: value.offset as usize, - size: value.size as usize, - repeat: value.repeat as usize, - } - } -} diff --git a/bindings/ruby/ext/ebur128_stream/src/samples.rs b/bindings/ruby/ext/ebur128_stream/src/samples.rs index cae4422..6f60204 100644 --- a/bindings/ruby/ext/ebur128_stream/src/samples.rs +++ b/bindings/ruby/ext/ebur128_stream/src/samples.rs @@ -1,13 +1,9 @@ -use crate::{ - error::Error, - memory_view::{Flags, FlagsChainable, ItemComponent, MemoryView}, -}; +use crate::error::Error; +use grey_knights::memory_view::{Flags, FlagsChainable, ItemComponent, ValidatedMemoryView}; use magnus::{RArray, Ruby, TryConvert, Value, error::IntoError}; fn is_acceptable_component(component: ItemComponent) -> bool { - component.offset == 0 - && component.repeat == 1 - && is_acceptable_format(component.format) + component.offset == 0 && component.repeat == 1 && is_acceptable_format(component.format) } fn is_acceptable_format(format: char) -> bool { @@ -20,13 +16,13 @@ fn is_acceptable_format(format: char) -> bool { #[cfg(target_endian = "big")] 'g' => true, - _ => false + _ => false, } } pub(crate) enum InterleavedSamples { Array { samples: Vec }, - MemoryView { view: MemoryView }, + MemoryView { view: ValidatedMemoryView }, } impl TryConvert for InterleavedSamples { @@ -52,14 +48,14 @@ impl InterleavedSamples { } } - fn consume_memory_view(val: Value) -> Option> { - let view = MemoryView::::get(val, Flags::any_contiguous()); + fn consume_memory_view(val: Value) -> Option> { + let view = ValidatedMemoryView::::new(val, Flags::any_contiguous()); if let Ok(mut view) = view { if Self::is_acceptable(&mut view).unwrap_or(false) { return Some(view); } } - let view = MemoryView::::get(val, Flags::simple()); + let view = ValidatedMemoryView::::new(val, Flags::simple()); if let Ok(mut view) = view { if Self::is_acceptable(&mut view).unwrap_or(false) { return Some(view); @@ -69,7 +65,7 @@ impl InterleavedSamples { } // TODO: Check format more strictly(size, other expression) - fn is_acceptable(view: &mut MemoryView) -> Result { + fn is_acceptable(view: &mut ValidatedMemoryView) -> Result { let item_desc = view.item_desc()?; Ok(view.ndim() == 1 && item_desc.len() == 1 @@ -82,7 +78,7 @@ impl InterleavedSamples { pub(crate) enum WritableInterleavedSamples { Array { obj: RArray, samples: Vec }, - MemoryView { view: MemoryView }, + MemoryView { view: ValidatedMemoryView }, } impl TryConvert for WritableInterleavedSamples { @@ -121,14 +117,14 @@ impl WritableInterleavedSamples { Ok(()) } - fn consume_memory_view(val: Value) -> Option> { - let view = MemoryView::::get(val, Flags::writable().any_contiguous()); + fn consume_memory_view(val: Value) -> Option> { + let view = ValidatedMemoryView::::new(val, Flags::writable().any_contiguous()); if let Ok(mut view) = view { if Self::is_acceptable(&mut view).unwrap_or(false) { return Some(view); } } - let view = MemoryView::::get(val, Flags::simple()); + let view = ValidatedMemoryView::::new(val, Flags::simple()); if let Ok(mut view) = view { if !view.is_readonly() && Self::is_acceptable(&mut view).unwrap_or(false) { return Some(view); @@ -137,7 +133,7 @@ impl WritableInterleavedSamples { None } - fn is_acceptable(view: &mut MemoryView) -> Result { + fn is_acceptable(view: &mut ValidatedMemoryView) -> Result { let item_desc = view.item_desc()?; Ok(view.ndim() == 1 && item_desc.len() == 1 @@ -150,7 +146,7 @@ impl WritableInterleavedSamples { pub(crate) enum PlanarSamples { Array { samples: Vec> }, - MemoryView { view: MemoryView }, + MemoryView { view: ValidatedMemoryView }, } impl TryConvert for PlanarSamples { @@ -185,14 +181,14 @@ impl PlanarSamples { } } - fn consume_memory_view(val: Value) -> Option> { - let view = MemoryView::::get(val, Flags::row_major()); + fn consume_memory_view(val: Value) -> Option> { + let view = ValidatedMemoryView::::new(val, Flags::row_major()); if let Ok(mut view) = view { if Self::is_acceptable(&mut view).unwrap_or(false) { return Some(view); } } - let view = MemoryView::::get(val, Flags::simple()); + let view = ValidatedMemoryView::::new(val, Flags::simple()); if let Ok(mut view) = view { if Self::is_acceptable(&mut view).unwrap_or(false) { return Some(view); @@ -201,7 +197,7 @@ impl PlanarSamples { None } - fn is_acceptable(view: &mut MemoryView) -> Result { + fn is_acceptable(view: &mut ValidatedMemoryView) -> Result { let item_desc = view.item_desc()?; Ok(view.ndim() == 2 && item_desc.len() == 1