ruby_dsp 0.0.7 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 413c7e687de2f9253fc92e7d57b1a9a9facc40a1597ffd0c6ce9135f1d4eaeb4
4
- data.tar.gz: 816c5a826c06f77e9a306762185f26323ff0a06894b0d06e6669d39289295cdd
3
+ metadata.gz: d0352100ed3bc4d5984dd4b59ca7540eea006a7234eb2ed9a87ca88c61af3c91
4
+ data.tar.gz: 94b1b8182701c588618dbbe6e44f9135a73c62c9b8f174d745f5e807df379145
5
5
  SHA512:
6
- metadata.gz: a750759a4c53edf49001f021898c39d0c1ca473f64aa1a68983267b04117fb2e339867778c72d778891bac23d2ef44a42ace5cd8ae56c82feda4616dcb155fd9
7
- data.tar.gz: c1744710c8b0f5a99f0a46dcf40fb91159d13a976c8f4fe0cef1751e85e457a07743924c90dba30916cced84760c74fd56ce6b2c82692aba01906b033fb9bc14
6
+ metadata.gz: 17e977dd018f850f37210a0d4cc9abd209ad276f51f62792fb240ccb94a8d096f8f8ff7d59a201b21cab38e048b901ed93e4ff9c89809c281d2b923816bb2641
7
+ data.tar.gz: d807466791df6a0f6cac661dc77e763cd03c5aeb2dd251d969054ff2bda5d0321735f4615819233f9ed9fdb9bb51f3b8b0eaf2e886d7269f29c1595aa616bc0d
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # [RubyDSP](https://github.com/cichrrad/rubyDSP) | [Documentation](https://www.rubydoc.info/gems/ruby_dsp/0.0.7)
1
+ # [RubyDSP](https://github.com/cichrrad/rubyDSP) | [Documentation](https://www.rubydoc.info/gems/ruby_dsp/0.0.8)
2
2
 
3
3
  [![Ruby CI](https://github.com/cichrrad/rubyDSP/actions/workflows/test.yml/badge.svg)](https://github.com/cichrrad/rubyDSP/actions/workflows/test.yml)
4
4
 
@@ -19,6 +19,8 @@ I made this gem to try hands-on binding these two languages together, as I would
19
19
 
20
20
  * **Audio Synthesis & Sequencing:** Build multitrack, polyphonic audio from scratch using a (very limited) set of built-in mathematically generated waveforms (sine, square, sawtooth, and white noise).
21
21
 
22
+ * **DSP Filters & EQ:** A suite of filters (Low-pass, High-pass, Band-pass, Notch, and Shelving EQs) via `miniaudio`.
23
+
22
24
  * **Format Agnostic Loading:** Automatically decodes standard audio formats (WAV, MP3, FLAC) via `miniaudio`.
23
25
  > **Note:** While the loading of these formats is supported, `miniaudio` saves only in `.wav`. While other encodings might be considered in the future, they would require more dependencies and thus are not available right now.
24
26
 
@@ -26,6 +28,16 @@ I made this gem to try hands-on binding these two languages together, as I would
26
28
 
27
29
  * **YARD Support:** Includes pure-Ruby stubs (in `stubs`, duh) for IDE autocomplete and inline documentation.
28
30
 
31
+ ## Overview of Capabilities
32
+
33
+ * **File Operations:** `save_track`
34
+ * **Mutations:** `to_mono!`, `resample!`, `trim_silence!`, `normalize!`, `fade_in!`, `fade_out!`, `pad!`, `pad_to_duration!`, `clip!`
35
+ * **DSP Filters:** `low_pass!`, `high_pass!`, `band_pass!`, `notch!`, `peak_eq!`, `low_shelf!`, `high_shelf!`
36
+ * **Synthesis:** `add_wave!`
37
+ * **Analysis:** `duration`, `peak_amp`, `rms`, `framed_rms`, `zcr`, `framed_zcr`, `silence_bounds`
38
+
39
+ *(For full parameters and usage, please check the [API Documentation](https://www.rubydoc.info/gems/ruby_dsp/0.0.8))*
40
+
29
41
  ## Benchmarks & Performance
30
42
 
31
43
  My primary motivation (love for Ruby aside) was to try and bring C++ speed to Ruby, avoiding the massive memory and garbage collection overhead of native Ruby math for audio/DSP tasks.
@@ -41,10 +53,10 @@ Benchmark script files are in `benchmark` directory. If you clone the repo for d
41
53
  -----------------------------------------------------------------------------------------------
42
54
  | Benchmark | C++ Speedup | Ruby Allocation | C++ Allocation |
43
55
  -----------------------------------------------------------------------------------------------
44
- | Read-Only (RMS) | 49.07x | 40 B (1 obj) | 64 B (1 obj) |
45
- | Mutation (Normalize) | 462.51x | 3,528,040 B (1 obj) | 0 B (0 obj) |
46
- | Complex (Framed RMS) | 49.25x | 41,264 B (860 obj) | 64 B (1 obj) |
47
- | Dynamic (Add Wave) | 11.73x | N/A | N/A |
56
+ | Read-Only (RMS) | 48.71x | 40 B (1 obj) | 64 B (1 obj) |
57
+ | Mutation (Normalize) | 69.47x | 3,528,040 B (1 obj) | 0 B (0 obj) |
58
+ | Complex (Framed RMS) | 48.68x | 41,264 B (860 obj) | 64 B (1 obj) |
59
+ | Dynamic (Add Wave) | 11.67x | N/A | N/A |
48
60
  -----------------------------------------------------------------------------------------------
49
61
  ```
50
62
  ### Why is it faster?
@@ -75,7 +87,7 @@ $ gem install ruby_dsp
75
87
 
76
88
  *(Note: Installing this gem requires a modern C++ compiler, as it builds the native extensions directly on your machine upon installation. It requires Ruby 3.0+).*
77
89
 
78
- ## Quick Start: Audio Processing
90
+ ## Audio Processing
79
91
 
80
92
  Here is a quick look at what you can do with a loaded `AudioTrack`. Thanks to the fluent API, you can process audio in a single readable chain:
81
93
 
@@ -91,6 +103,8 @@ puts track
91
103
  # Process, edit, and save in one chain
92
104
  track.to_mono! # Averages channels into mono
93
105
  .resample!(44100) # Linearly resamples to target rate
106
+ .high_pass!(100) # Cuts out low-end rumble below 100Hz
107
+ .peak_eq!(1000, 3.0) # Boosts 1000Hz by 3dB for vocal clarity
94
108
  .trim_silence!(-60.0) # Strips leading/trailing silence below -60dB
95
109
  .normalize!(-1.0) # Scales audio to target peak dBFS
96
110
  .pad_to_duration!(15.0) # Centers audio evenly into a 15s window
@@ -98,7 +112,7 @@ track.to_mono! # Averages channels into mono
98
112
  .fade_out!(0.5) # Adds a 0.5s linear fade-out
99
113
  .save_track("processed.wav") # Export the final result
100
114
 
101
- # Analysis & Math (Still works!)
115
+ # Analysis & Math
102
116
  puts "Peak Amp: #{track.peak_amp}"
103
117
  puts "Overall RMS: #{track.rms}"
104
118
  puts "Overall ZCR: #{track.zcr}"
@@ -107,7 +121,7 @@ puts "Overall ZCR: #{track.zcr}"
107
121
  framed_rms_data = track.framed_rms(frame_length: 2048, hop_length: 512)
108
122
  ```
109
123
 
110
- ## Quick Start: Synthesis & Sequencing
124
+ ## Synthesis & Sequencing
111
125
 
112
126
  Initialize an empty track and generate your own jam using `add_wave!`:
113
127
 
@@ -133,28 +147,17 @@ track.normalize!(-30.0) # This goes a long way for your hearing
133
147
 
134
148
  #### **TWINKLE**
135
149
 
136
-
137
-
138
150
  https://github.com/user-attachments/assets/af6dbaee-630f-49e3-9704-8ff3440334cb
139
151
 
140
-
141
-
142
152
  #### **TETRIS**
143
153
 
144
-
145
-
146
154
  https://github.com/user-attachments/assets/b3ad6886-3552-4200-b725-f14083f96792
147
155
 
148
-
149
-
150
156
  #### **SUPER MARIO BROS**
151
157
 
152
-
153
-
154
158
  https://github.com/user-attachments/assets/5193d72d-4c32-4c83-8253-206402ac2889
155
159
 
156
160
 
157
-
158
161
  ## Development
159
162
 
160
163
  If you want to clone the repo and work on C++ guts, start with:
@@ -170,7 +170,7 @@ struct AudioTrack
170
170
  float max_val = 0.0f;
171
171
  for (const auto &sample : samples)
172
172
  {
173
- max_val = std::max(max_val, std::fabs(sample));
173
+ max_val = std::fmax(max_val, std::fabs(sample));
174
174
  }
175
175
  return max_val;
176
176
  }
@@ -592,7 +592,7 @@ struct AudioTrack
592
592
  float scale_factor = target_linear / current_peak;
593
593
 
594
594
  // already at the target peak -- do nothing
595
- if (std::abs(scale_factor - 1.0f) < 1e-5f)
595
+ if (std::fabs(scale_factor - 1.0f) < 1e-5f)
596
596
  return *this;
597
597
 
598
598
  for (auto &sample : samples)
@@ -662,18 +662,13 @@ struct AudioTrack
662
662
  unsigned long long head_samples = head_frames * channels;
663
663
  unsigned long long tail_samples = tail_frames * channels;
664
664
 
665
- // pad the beginning
666
- if (head_samples > 0)
667
- {
668
- samples.insert(samples.begin(), head_samples, 0.0f);
669
- }
670
-
671
- // pad the end
672
- if (tail_samples > 0)
673
- {
674
- samples.insert(samples.end(), tail_samples, 0.0f);
675
- }
665
+ // calculate total new size
666
+ unsigned long long new_size = head_samples + sample_count + tail_samples;
667
+ std::vector<float> new_samples(new_size, 0.0f);
676
668
 
669
+ // move old samples in
670
+ std::move(samples.begin(), samples.end(), new_samples.begin() + head_samples);
671
+ samples = std::move(new_samples);
677
672
  sample_count = samples.size();
678
673
 
679
674
  return *this;
@@ -700,18 +695,12 @@ struct AudioTrack
700
695
  unsigned long long head_samples = head_frames * channels;
701
696
  unsigned long long tail_samples = tail_frames * channels;
702
697
 
703
- // pad the beginning
704
- if (head_samples > 0)
705
- {
706
- samples.insert(samples.begin(), head_samples, 0.0f);
707
- }
708
-
709
- // pad the end
710
- if (tail_samples > 0)
711
- {
712
- samples.insert(samples.end(), tail_samples, 0.0f);
713
- }
698
+ unsigned long long new_size = head_samples + sample_count + tail_samples;
699
+ std::vector<float> new_samples(new_size, 0.0f);
714
700
 
701
+ // move old samples in
702
+ std::move(samples.begin(), samples.end(), new_samples.begin() + head_samples);
703
+ samples = std::move(new_samples);
715
704
  sample_count = samples.size();
716
705
 
717
706
  return *this;
@@ -775,6 +764,137 @@ struct AudioTrack
775
764
  return *this;
776
765
  }
777
766
 
767
+ AudioTrack &clip_bang()
768
+ {
769
+ for (auto &s : samples)
770
+ {
771
+ s = std::clamp(s, -1.0f, 1.0f);
772
+ }
773
+ return *this;
774
+ }
775
+
776
+ AudioTrack &low_pass_bang(int cutoffFreq)
777
+ {
778
+ // High order low-pass filter (Butterworth)
779
+ // [https://miniaud.io/docs/manual/index.html#Filtering]
780
+ ma_lpf lpf;
781
+ ma_lpf_config config;
782
+
783
+ // last argument is MA_MAX_FILTER_ORDER, goes up to 4?
784
+ config = ma_lpf_config_init(ma_format_f32, channels, sample_rate, (double)cutoffFreq, 2);
785
+
786
+ if (ma_lpf_init(&config, NULL, &lpf) != MA_SUCCESS)
787
+ {
788
+ throw std::runtime_error("RubyDSP: Failed to initialize low-pass filter.");
789
+ }
790
+
791
+ // in-place filtering
792
+ unsigned long long frame_count = sample_count / channels;
793
+ ma_lpf_process_pcm_frames(&lpf, samples.data(), samples.data(), frame_count);
794
+ ma_lpf_uninit(&lpf, NULL);
795
+
796
+ return *this;
797
+ }
798
+
799
+ AudioTrack &high_pass_bang(int cutoff_freq)
800
+ {
801
+ ma_hpf hpf;
802
+ // 2nd order high-pass
803
+ ma_hpf_config config = ma_hpf_config_init(ma_format_f32, channels, sample_rate, (double)cutoff_freq, 2);
804
+
805
+ if (ma_hpf_init(&config, NULL, &hpf) != MA_SUCCESS)
806
+ {
807
+ throw std::runtime_error("RubyDSP: Failed to initialize high-pass filter.");
808
+ }
809
+
810
+ unsigned long long frame_count = sample_count / channels;
811
+ ma_hpf_process_pcm_frames(&hpf, samples.data(), samples.data(), frame_count);
812
+
813
+ return *this;
814
+ }
815
+
816
+ AudioTrack &band_pass_bang(int cutoff_freq)
817
+ {
818
+ ma_bpf bpf;
819
+ // order MUST be even for band-pass
820
+ // [https://miniaud.io/docs/manual/index.html#Filtering]
821
+ ma_bpf_config config = ma_bpf_config_init(ma_format_f32, channels, sample_rate, (double)cutoff_freq, 2);
822
+
823
+ if (ma_bpf_init(&config, NULL, &bpf) != MA_SUCCESS)
824
+ {
825
+ throw std::runtime_error("RubyDSP: Failed to initialize band-pass filter.");
826
+ }
827
+
828
+ unsigned long long frame_count = sample_count / channels;
829
+ ma_bpf_process_pcm_frames(&bpf, samples.data(), samples.data(), frame_count);
830
+
831
+ return *this;
832
+ }
833
+
834
+ AudioTrack &notch_bang(int center_freq, double q = 0.707)
835
+ {
836
+ ma_notch2 notch;
837
+ ma_notch2_config config = ma_notch2_config_init(ma_format_f32, channels, sample_rate, q, (double)center_freq);
838
+
839
+ if (ma_notch2_init(&config, NULL, &notch) != MA_SUCCESS)
840
+ {
841
+ throw std::runtime_error("RubyDSP: Failed to initialize notch filter.");
842
+ }
843
+
844
+ unsigned long long frame_count = sample_count / channels;
845
+ ma_notch2_process_pcm_frames(&notch, samples.data(), samples.data(), frame_count);
846
+
847
+ return *this;
848
+ }
849
+
850
+ AudioTrack &peak_eq_bang(int center_freq, double gain_db, double q = 0.707)
851
+ {
852
+ ma_peak2 peak;
853
+ ma_peak2_config config = ma_peak2_config_init(ma_format_f32, channels, sample_rate, gain_db, q, (double)center_freq);
854
+
855
+ if (ma_peak2_init(&config, NULL, &peak) != MA_SUCCESS)
856
+ {
857
+ throw std::runtime_error("RubyDSP: Failed to initialize peaking EQ filter.");
858
+ }
859
+
860
+ unsigned long long frame_count = sample_count / channels;
861
+ ma_peak2_process_pcm_frames(&peak, samples.data(), samples.data(), frame_count);
862
+
863
+ return *this;
864
+ }
865
+
866
+ AudioTrack &low_shelf_bang(int cutoff_freq, double gain_db, double q = 0.707)
867
+ {
868
+ ma_loshelf2 shelf;
869
+ ma_loshelf2_config config = ma_loshelf2_config_init(ma_format_f32, channels, sample_rate, gain_db, q, (double)cutoff_freq);
870
+
871
+ if (ma_loshelf2_init(&config, NULL, &shelf) != MA_SUCCESS)
872
+ {
873
+ throw std::runtime_error("RubyDSP: Failed to initialize low-shelf filter.");
874
+ }
875
+
876
+ unsigned long long frame_count = sample_count / channels;
877
+ ma_loshelf2_process_pcm_frames(&shelf, samples.data(), samples.data(), frame_count);
878
+
879
+ return *this;
880
+ }
881
+
882
+ AudioTrack &high_shelf_bang(int cutoff_freq, double gain_db, double q = 0.707)
883
+ {
884
+ ma_hishelf2 shelf;
885
+ ma_hishelf2_config config = ma_hishelf2_config_init(ma_format_f32, channels, sample_rate, gain_db, q, (double)cutoff_freq);
886
+
887
+ if (ma_hishelf2_init(&config, NULL, &shelf) != MA_SUCCESS)
888
+ {
889
+ throw std::runtime_error("RubyDSP: Failed to initialize high-shelf filter.");
890
+ }
891
+
892
+ unsigned long long frame_count = sample_count / channels;
893
+ ma_hishelf2_process_pcm_frames(&shelf, samples.data(), samples.data(), frame_count);
894
+
895
+ return *this;
896
+ }
897
+
778
898
  std::string to_s()
779
899
  {
780
900
  std::ostringstream stream;
@@ -849,5 +969,27 @@ extern "C"
849
969
  Arg("duration_sec"),
850
970
  Arg("start_sec") = -1.0f,
851
971
  Arg("amplitude") = 1.0f)
972
+ .define_method("clip!", &AudioTrack::clip_bang)
973
+ .define_method("low_pass!", &AudioTrack::low_pass_bang,
974
+ Arg("cutoff_freq"))
975
+ .define_method("high_pass!", &AudioTrack::high_pass_bang,
976
+ Arg("cutoff_freq"))
977
+ .define_method("band_pass!", &AudioTrack::band_pass_bang,
978
+ Arg("cutoff_freq"))
979
+ .define_method("notch!", &AudioTrack::notch_bang,
980
+ Arg("center_freq"),
981
+ Arg("q") = 0.707)
982
+ .define_method("peak_eq!", &AudioTrack::peak_eq_bang,
983
+ Arg("center_freq"),
984
+ Arg("gain_db"),
985
+ Arg("q") = 0.707)
986
+ .define_method("low_shelf!", &AudioTrack::low_shelf_bang,
987
+ Arg("cutoff_freq"),
988
+ Arg("gain_db"),
989
+ Arg("q") = 0.707)
990
+ .define_method("high_shelf!", &AudioTrack::high_shelf_bang,
991
+ Arg("cutoff_freq"),
992
+ Arg("gain_db"),
993
+ Arg("q") = 0.707)
852
994
  .define_method("to_s", &AudioTrack::to_s);
853
995
  }
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module RubyDSP
5
- VERSION = '0.0.7'
5
+ VERSION = '0.0.8'
6
6
  end
@@ -179,6 +179,80 @@ module RubyDSP
179
179
  def add_wave!(wave_type, frequency, duration_sec, start_sec = -1.0, amplitude = 1.0)
180
180
  end
181
181
 
182
+ # Destructively clamps all audio samples to the standard [-1.0, 1.0] range.
183
+ #
184
+ # This prevents harsh digital clipping when exporting polyphonic or boosted audio.
185
+ #
186
+ # @return [AudioTrack] self for method chaining.
187
+ def clip!
188
+ end
189
+
190
+ # Applies a high-order low-pass filter (Butterworth) to the track.
191
+ #
192
+ # Attenuates frequencies above the cutoff, letting lower frequencies pass through.
193
+ #
194
+ # @param cutoff_freq [Integer, Float] The threshold frequency in Hz.
195
+ # @return [AudioTrack] self for method chaining.
196
+ def low_pass!(cutoff_freq)
197
+ end
198
+
199
+ # Applies a 2nd-order high-pass filter to the track.
200
+ #
201
+ # Attenuates frequencies below the cutoff, letting higher frequencies pass through.
202
+ #
203
+ # @param cutoff_freq [Integer, Float] The threshold frequency in Hz.
204
+ # @return [AudioTrack] self for method chaining.
205
+ def high_pass!(cutoff_freq)
206
+ end
207
+
208
+ # Applies a 2nd-order band-pass filter to the track.
209
+ #
210
+ # Preserves frequencies around the cutoff, heavily attenuating both higher and lower frequencies.
211
+ #
212
+ # @param cutoff_freq [Integer, Float] The center frequency in Hz.
213
+ # @return [AudioTrack] self for method chaining.
214
+ def band_pass!(cutoff_freq)
215
+ end
216
+
217
+ # Applies a 2nd-order notch filter to surgically remove a specific frequency.
218
+ #
219
+ # @param center_freq [Integer, Float] The exact frequency to eliminate in Hz.
220
+ # @param q [Float] The resonance/width of the notch. Defaults to 0.707 (Butterworth).
221
+ # @return [AudioTrack] self for method chaining.
222
+ def notch!(center_freq, q = 0.707)
223
+ end
224
+
225
+ # Applies a 2nd-order peaking EQ filter to boost or cut a specific frequency band.
226
+ #
227
+ # @param center_freq [Integer, Float] The target frequency in Hz.
228
+ # @param gain_db [Float] The amount to boost (positive) or cut (negative) in decibels.
229
+ # @param q [Float] The resonance/width of the bell curve. Defaults to 0.707.
230
+ # @return [AudioTrack] self for method chaining.
231
+ def peak_eq!(center_freq, gain_db, q = 0.707)
232
+ end
233
+
234
+ # Applies a 2nd-order low-shelf filter.
235
+ #
236
+ # Boosts or cuts frequencies below the cutoff without affecting higher frequencies.
237
+ #
238
+ # @param cutoff_freq [Integer, Float] The threshold frequency in Hz.
239
+ # @param gain_db [Float] The amount to boost (positive) or cut (negative) in decibels.
240
+ # @param q [Float] The resonance/slope of the shelf. Defaults to 0.707.
241
+ # @return [AudioTrack] self for method chaining.
242
+ def low_shelf!(cutoff_freq, gain_db, q = 0.707)
243
+ end
244
+
245
+ # Applies a 2nd-order high-shelf filter.
246
+ #
247
+ # Boosts or cuts frequencies above the cutoff without affecting lower frequencies.
248
+ #
249
+ # @param cutoff_freq [Integer, Float] The threshold frequency in Hz.
250
+ # @param gain_db [Float] The amount to boost (positive) or cut (negative) in decibels.
251
+ # @param q [Float] The resonance/slope of the shelf. Defaults to 0.707.
252
+ # @return [AudioTrack] self for method chaining.
253
+ def high_shelf!(cutoff_freq, gain_db, q = 0.707)
254
+ end
255
+
182
256
  # @return [String] a formatted summary of the track.
183
257
  def to_s
184
258
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_dsp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.7
4
+ version: 0.0.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Radek C.