ruby_dsp 0.0.6 → 0.0.7
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 +4 -4
- data/.gitignore +2 -1
- data/README.md +50 -18
- data/Rakefile +15 -0
- data/benchmark/bench.rb +189 -0
- data/benchmark/logs/.gitkeep +0 -0
- data/benchmark/parse_log.rb +73 -0
- data/demo/demo_mbros.rb +23 -0
- data/demo/demo_tetris.rb +27 -0
- data/demo/demo_twinkle.rb +33 -0
- data/demo/mario_intro.mp4 +0 -0
- data/demo/tetris_theme.mp4 +0 -0
- data/demo/twinkle_twinkle.mp4 +0 -0
- data/ext/ruby_dsp/ruby_dsp.cpp +0 -1
- data/lib/ruby_dsp/version.rb +1 -1
- data/ruby_dsp.gemspec +3 -0
- metadata +52 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 413c7e687de2f9253fc92e7d57b1a9a9facc40a1597ffd0c6ce9135f1d4eaeb4
|
|
4
|
+
data.tar.gz: 816c5a826c06f77e9a306762185f26323ff0a06894b0d06e6669d39289295cdd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a750759a4c53edf49001f021898c39d0c1ca473f64aa1a68983267b04117fb2e339867778c72d778891bac23d2ef44a42ace5cd8ae56c82feda4616dcb155fd9
|
|
7
|
+
data.tar.gz: c1744710c8b0f5a99f0a46dcf40fb91159d13a976c8f4fe0cef1751e85e457a07743924c90dba30916cced84760c74fd56ce6b2c82692aba01906b033fb9bc14
|
data/.gitignore
CHANGED
data/README.md
CHANGED
|
@@ -1,54 +1,76 @@
|
|
|
1
|
-
# RubyDSP | [Documentation](https://www.rubydoc.info/gems/ruby_dsp/0.0.
|
|
1
|
+
# [RubyDSP](https://github.com/cichrrad/rubyDSP) | [Documentation](https://www.rubydoc.info/gems/ruby_dsp/0.0.7)
|
|
2
2
|
|
|
3
3
|
[](https://github.com/cichrrad/rubyDSP/actions/workflows/test.yml)
|
|
4
4
|
|
|
5
|
-
---
|
|
6
|
-
|
|
7
5
|
> 🚧 **Status:** This (***HOBBY***) project is currently in early development. It is hopefully functional, but API changes are expected. There is no warranty regarding anything 🗿.
|
|
8
6
|
|
|
9
7
|
---
|
|
10
8
|
|
|
11
9
|
**RubyDSP** is an audio processing/synthesis and DSP Ruby gem made mostly for fun. It uses C++ under the hood, utilizing [miniaudio](https://miniaud.io/) and [Rice](https://github.com/jasonroelofs/rice).
|
|
10
|
+
I made this gem to try hands-on binding these two languages together, as I would like to be able to bring some C++ speed to Ruby, oh my beloved....
|
|
12
11
|
|
|
13
|
-
|
|
12
|
+
> **Note** that AI (Gemini) was used in the developement of this software, albeit only partially and with supervision and testing. If this goes against your ideals, I am sorry.
|
|
14
13
|
|
|
15
14
|
## Features
|
|
16
15
|
|
|
17
|
-
* **Fast:** Basically all of the code is written in C++. While not extremely optimized currently, it still absolutely shreds native Ruby.
|
|
16
|
+
* **Fast:** Basically all of the code is written in C++. While not extremely optimized currently, it still absolutely shreds native Ruby (see [Benchmark section](#benchmarks--performance)).
|
|
18
17
|
|
|
19
18
|
* **Fluent API:** Mutating methods return `self`, allowing for beautiful, highly readable method chaining (e.g., `track.to_mono!.normalize!.fade_in!(0.5)`).
|
|
20
19
|
|
|
21
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).
|
|
22
21
|
|
|
23
22
|
* **Format Agnostic Loading:** Automatically decodes standard audio formats (WAV, MP3, FLAC) via `miniaudio`.
|
|
24
|
-
|
|
25
|
-
> **Note:** While the loading of these formats is supported, `miniaudio` encodes only in `.wav`. While other encodings might be considered in the future, they would require more dependencies and thus are not available right now.
|
|
23
|
+
> **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.
|
|
26
24
|
|
|
27
25
|
* **Zero-Dependency Native Build:** No need to install `ffmpeg` or `libsndfile` on your system.
|
|
28
26
|
|
|
29
27
|
* **YARD Support:** Includes pure-Ruby stubs (in `stubs`, duh) for IDE autocomplete and inline documentation.
|
|
30
28
|
|
|
29
|
+
## Benchmarks & Performance
|
|
30
|
+
|
|
31
|
+
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.
|
|
32
|
+
|
|
33
|
+
To back up my words, `RubyDSP` was benchmarked against optimized pure Ruby mock implementation (in `benchmark/bench.rb`), where MRI's internal C-backed methods like `Array#sum` and `Array#fill` were preffered over *prettier* ways of coding. The tests were run on a 10-second mono audio track at 44.1kHz (i.e., `441,000` samples) generated by the gem itself.
|
|
34
|
+
|
|
35
|
+
Benchmark script files are in `benchmark` directory. If you clone the repo for development, you can do `rake bench:summary` from the root directory to re-run the benchmark on your machine.
|
|
36
|
+
|
|
37
|
+
### Benchmark Output
|
|
38
|
+
|
|
39
|
+
> CPU: AMD RYZEN 5 5600X
|
|
40
|
+
```bash
|
|
41
|
+
-----------------------------------------------------------------------------------------------
|
|
42
|
+
| Benchmark | C++ Speedup | Ruby Allocation | C++ Allocation |
|
|
43
|
+
-----------------------------------------------------------------------------------------------
|
|
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 |
|
|
48
|
+
-----------------------------------------------------------------------------------------------
|
|
49
|
+
```
|
|
50
|
+
### Why is it faster?
|
|
51
|
+
|
|
52
|
+
* **Zero GC Overhead on Mutations:** When you modify an array in pure Ruby (e.g., `samples.map!`), Ruby must dynamically allocate hundreds of thousands of new `Float` objects, heavily taxing the Garbage Collector. Thanks to [Rice stl mapping](https://ruby-rice.github.io/4.x/stl/stl/), `RubyDSP` loops over a contiguous block of memory and mutates raw 32-bit floats natively in place, resulting in exactly zero Ruby allocations.
|
|
53
|
+
|
|
54
|
+
* **No Sub-Array Slicing:** For sliding window calculations like `framed_rms`, pure Ruby creates hundreds of intermediate sub-arrays. C++ calculates the windows directly and returns a single, lightweight proxy object to Ruby via Rice.
|
|
55
|
+
|
|
31
56
|
## Installation
|
|
32
57
|
|
|
33
58
|
Add this line to your application's `Gemfile`:
|
|
34
59
|
|
|
35
60
|
```ruby
|
|
36
61
|
gem 'ruby_dsp'
|
|
37
|
-
|
|
38
62
|
```
|
|
39
63
|
|
|
40
64
|
And then execute:
|
|
41
65
|
|
|
42
66
|
```bash
|
|
43
67
|
$ bundle install
|
|
44
|
-
|
|
45
68
|
```
|
|
46
69
|
|
|
47
70
|
Or install it yourself directly via:
|
|
48
71
|
|
|
49
72
|
```bash
|
|
50
73
|
$ gem install ruby_dsp
|
|
51
|
-
|
|
52
74
|
```
|
|
53
75
|
|
|
54
76
|
*(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+).*
|
|
@@ -61,7 +83,7 @@ Here is a quick look at what you can do with a loaded `AudioTrack`. Thanks to th
|
|
|
61
83
|
require 'ruby_dsp'
|
|
62
84
|
|
|
63
85
|
# Load an audio file
|
|
64
|
-
track = RubyDSP::AudioTrack.new("
|
|
86
|
+
track = RubyDSP::AudioTrack.new("raw_voC++ backend cals.wav")
|
|
65
87
|
|
|
66
88
|
puts track
|
|
67
89
|
# => ['raw_vocals.wav', 12.450s duration, 2 channel(s), 48000Hz sample rate]
|
|
@@ -83,7 +105,6 @@ puts "Overall ZCR: #{track.zcr}"
|
|
|
83
105
|
|
|
84
106
|
# You can also get framed analysis for time-series data:
|
|
85
107
|
framed_rms_data = track.framed_rms(frame_length: 2048, hop_length: 512)
|
|
86
|
-
|
|
87
108
|
```
|
|
88
109
|
|
|
89
110
|
## Quick Start: Synthesis & Sequencing
|
|
@@ -103,8 +124,7 @@ track.add_wave!("sine", 261.63, 3.0, 0.0)
|
|
|
103
124
|
# Polish and export
|
|
104
125
|
track.normalize!(-30.0) # This goes a long way for your hearing
|
|
105
126
|
.fade_out!(0.5) # Smooth tail
|
|
106
|
-
.save_track("c_major_chord.wav")
|
|
107
|
-
|
|
127
|
+
.save_track("c_major_chord.wav")J
|
|
108
128
|
```
|
|
109
129
|
|
|
110
130
|
### Demos
|
|
@@ -113,15 +133,27 @@ track.normalize!(-30.0) # This goes a long way for your hearing
|
|
|
113
133
|
|
|
114
134
|
#### **TWINKLE**
|
|
115
135
|
|
|
116
|
-
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
https://github.com/user-attachments/assets/af6dbaee-630f-49e3-9704-8ff3440334cb
|
|
139
|
+
|
|
140
|
+
|
|
117
141
|
|
|
118
142
|
#### **TETRIS**
|
|
119
143
|
|
|
120
|
-
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
https://github.com/user-attachments/assets/b3ad6886-3552-4200-b725-f14083f96792
|
|
147
|
+
|
|
148
|
+
|
|
121
149
|
|
|
122
150
|
#### **SUPER MARIO BROS**
|
|
123
151
|
|
|
124
|
-
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
https://github.com/user-attachments/assets/5193d72d-4c32-4c83-8253-206402ac2889
|
|
155
|
+
|
|
156
|
+
|
|
125
157
|
|
|
126
158
|
## Development
|
|
127
159
|
|
|
@@ -137,4 +169,4 @@ The gem is available as open source under the terms of the **MIT License**.
|
|
|
137
169
|
|
|
138
170
|
---
|
|
139
171
|
|
|
140
|
-
> Cheers! - RC
|
|
172
|
+
> Cheers! - RC
|
data/Rakefile
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
require 'bundler/gem_tasks'
|
|
3
3
|
require 'rake/extensiontask'
|
|
4
4
|
require 'rake/testtask'
|
|
5
|
+
require 'securerandom'
|
|
6
|
+
|
|
7
|
+
namespace :bench do
|
|
8
|
+
desc 'Run benchmark, save to UUID log, and display a parsed summary'
|
|
9
|
+
task :summary do
|
|
10
|
+
log_filename = "benchmark/logs/bench_#{SecureRandom.uuid}.log"
|
|
11
|
+
|
|
12
|
+
puts 'Running benchmarks... This will take a moment (~1 min).'
|
|
13
|
+
system("bundle exec ruby benchmark/bench.rb > #{log_filename}")
|
|
14
|
+
|
|
15
|
+
puts "Done! Parsing results from #{log_filename}...\n\n"
|
|
16
|
+
|
|
17
|
+
system("bundle exec ruby benchmark/parse_log.rb #{log_filename}")
|
|
18
|
+
end
|
|
19
|
+
end
|
|
5
20
|
|
|
6
21
|
Rake::ExtensionTask.new('ruby_dsp') do |ext|
|
|
7
22
|
ext.lib_dir = 'lib/ruby_dsp'
|
data/benchmark/bench.rb
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# frozen_string_literal: false
|
|
2
|
+
|
|
3
|
+
require 'benchmark/ips'
|
|
4
|
+
require 'ruby_dsp'
|
|
5
|
+
require 'memory_profiler'
|
|
6
|
+
|
|
7
|
+
# MOCK NATIVE RUBY IMPLEMENTATION for 1 channel audio
|
|
8
|
+
class PureRubyAudioTrack
|
|
9
|
+
attr_accessor :samples, :channels, :sample_rate
|
|
10
|
+
|
|
11
|
+
def initialize(channels = 1, sample_rate = 44_100)
|
|
12
|
+
@channels = channels
|
|
13
|
+
@sample_rate = sample_rate
|
|
14
|
+
@samples = []
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Pure Ruby RMS calculation
|
|
18
|
+
def rms
|
|
19
|
+
return [0.0] if @samples.empty?
|
|
20
|
+
|
|
21
|
+
sum_sq = @samples.sum { |s| s * s }
|
|
22
|
+
[Math.sqrt(sum_sq / @samples.size)]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Pure Ruby array mutation
|
|
26
|
+
def normalize!(target_db = -1.0)
|
|
27
|
+
return self if @samples.empty?
|
|
28
|
+
|
|
29
|
+
peak = @samples.map(&:abs).max
|
|
30
|
+
return self if peak <= 0.0
|
|
31
|
+
|
|
32
|
+
target_linear = 10.0**(target_db / 20.0)
|
|
33
|
+
scale_factor = target_linear / peak
|
|
34
|
+
|
|
35
|
+
@samples.map! { |s| s * scale_factor }
|
|
36
|
+
self
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def duration
|
|
40
|
+
@samples.size.to_f / @sample_rate
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# optimized Pure Ruby framed RMS
|
|
44
|
+
def framed_rms(frame_length = 2048, hop_length = 512)
|
|
45
|
+
return [] if @samples.empty?
|
|
46
|
+
|
|
47
|
+
return [rms] if @samples.size < frame_length
|
|
48
|
+
|
|
49
|
+
expected_frames = ((@samples.size - frame_length) / hop_length) + 1
|
|
50
|
+
result = Array.new(expected_frames)
|
|
51
|
+
|
|
52
|
+
expected_frames.times do |i|
|
|
53
|
+
start_idx = i * hop_length
|
|
54
|
+
# Array slicing optimized in MRI
|
|
55
|
+
frame = @samples[start_idx, frame_length]
|
|
56
|
+
# .sum is implemented in C is faster than manual iteration
|
|
57
|
+
sum_sq = frame.sum { |s| s * s }
|
|
58
|
+
result[i] = Math.sqrt(sum_sq / frame_length)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
[result] # Wrap in 2D array to match our API
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# optimized Pure Ruby add_wave (just sine for the benchmark -- discards wave_type)
|
|
65
|
+
def add_wave!(_wave_type, frequency, duration_sec, start_sec = -1.0, amplitude = 1.0)
|
|
66
|
+
start_sec = duration if start_sec < 0.0
|
|
67
|
+
|
|
68
|
+
start_sample = (start_sec * @sample_rate).to_i
|
|
69
|
+
wave_samples = (duration_sec * @sample_rate).to_i
|
|
70
|
+
end_sample = start_sample + wave_samples
|
|
71
|
+
|
|
72
|
+
# memory pre-allocation using MRI's internal C fill
|
|
73
|
+
@samples.fill(0.0, @samples.size...end_sample) if end_sample > @samples.size
|
|
74
|
+
|
|
75
|
+
two_pi_freq = 2.0 * Math::PI * frequency
|
|
76
|
+
|
|
77
|
+
wave_samples.times do |i|
|
|
78
|
+
t = i.to_f / @sample_rate
|
|
79
|
+
val = Math.sin(two_pi_freq * t) * amplitude
|
|
80
|
+
|
|
81
|
+
@samples[start_sample + i] += val
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
self
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# TEST DATA
|
|
89
|
+
DURATION = 10.0
|
|
90
|
+
FREQ = 440.0
|
|
91
|
+
|
|
92
|
+
puts 'Generating test data...'
|
|
93
|
+
|
|
94
|
+
# Setup C++ Track
|
|
95
|
+
cpp_track = RubyDSP::AudioTrack.new('', 1, 44_100)
|
|
96
|
+
cpp_track.add_wave!('sine', FREQ, DURATION)
|
|
97
|
+
|
|
98
|
+
# Setup Native Ruby Track
|
|
99
|
+
ruby_track = PureRubyAudioTrack.new(1, 44_100)
|
|
100
|
+
ruby_track.add_wave!('sine', FREQ, DURATION)
|
|
101
|
+
|
|
102
|
+
puts "Data generated! Starting benchmarks...\n\n"
|
|
103
|
+
|
|
104
|
+
# RUNTIME
|
|
105
|
+
|
|
106
|
+
Benchmark.ips do |x|
|
|
107
|
+
x.config(time: 5, warmup: 2)
|
|
108
|
+
|
|
109
|
+
x.report('Read-Only (RMS) - Ruby') { ruby_track.rms }
|
|
110
|
+
x.report('Read-Only (RMS) - C++ ') { cpp_track.rms }
|
|
111
|
+
|
|
112
|
+
x.compare!
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
puts "\n#{'-' * 50}\n\n"
|
|
116
|
+
|
|
117
|
+
Benchmark.ips do |x|
|
|
118
|
+
x.config(time: 5, warmup: 2)
|
|
119
|
+
|
|
120
|
+
x.report('Mutation (Normalize) - Ruby') { ruby_track.normalize!(-3.0) }
|
|
121
|
+
x.report('Mutation (Normalize) - C++ ') { cpp_track.normalize!(-3.0) }
|
|
122
|
+
|
|
123
|
+
x.compare!
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
Benchmark.ips do |x|
|
|
127
|
+
x.config(time: 5, warmup: 2)
|
|
128
|
+
|
|
129
|
+
x.report('Complex (Framed RMS) - Ruby') { ruby_track.framed_rms(2048, 512) }
|
|
130
|
+
x.report('Complex (Framed RMS) - C++ ') { cpp_track.framed_rms(2048, 512) }
|
|
131
|
+
|
|
132
|
+
x.compare!
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
puts "\n#{'-' * 50}\n\n"
|
|
136
|
+
|
|
137
|
+
Benchmark.ips do |x|
|
|
138
|
+
x.config(time: 5, warmup: 2)
|
|
139
|
+
|
|
140
|
+
# Adding a 2-second wave into the middle of the track
|
|
141
|
+
x.report('Dynamic (Add Wave) - Ruby') { ruby_track.add_wave!('sine', 880.0, 2.0, 4.0, 0.5) }
|
|
142
|
+
x.report('Dynamic (Add Wave) - C++ ') { cpp_track.add_wave!('sine', 880.0, 2.0, 4.0, 0.5) }
|
|
143
|
+
|
|
144
|
+
x.compare!
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# MEMORY
|
|
148
|
+
|
|
149
|
+
puts "\n#{'=' * 50}"
|
|
150
|
+
puts 'MEMORY PROFILING: Read-Only (RMS)'
|
|
151
|
+
puts '=' * 50
|
|
152
|
+
|
|
153
|
+
# Profile Ruby RMS
|
|
154
|
+
puts "\n[ Profiling Ruby RMS... ]"
|
|
155
|
+
report_rms_ruby = MemoryProfiler.report { ruby_track.rms }
|
|
156
|
+
report_rms_ruby.pretty_print(scale_bytes: false, color_output: false)
|
|
157
|
+
|
|
158
|
+
# Profile C++ RMS
|
|
159
|
+
puts "\n[ Profiling C++ RMS... ]"
|
|
160
|
+
report_rms_cpp = MemoryProfiler.report { cpp_track.rms }
|
|
161
|
+
report_rms_cpp.pretty_print(scale_bytes: false, color_output: false)
|
|
162
|
+
|
|
163
|
+
puts "\n#{'=' * 50}"
|
|
164
|
+
puts 'MEMORY PROFILING: Mutation (Normalize)'
|
|
165
|
+
puts '=' * 50
|
|
166
|
+
|
|
167
|
+
# Profile Ruby Normalize
|
|
168
|
+
puts "\n[ Profiling Ruby Normalize... ]"
|
|
169
|
+
report_norm_ruby = MemoryProfiler.report { ruby_track.normalize!(-3.0) }
|
|
170
|
+
report_norm_ruby.pretty_print(scale_bytes: false, color_output: false)
|
|
171
|
+
|
|
172
|
+
# Profile C++ Normalize
|
|
173
|
+
puts "\n[ Profiling C++ Normalize... ]"
|
|
174
|
+
report_norm_cpp = MemoryProfiler.report { cpp_track.normalize!(-3.0) }
|
|
175
|
+
report_norm_cpp.pretty_print(scale_bytes: false, color_output: false)
|
|
176
|
+
|
|
177
|
+
puts "\n#{'=' * 50}"
|
|
178
|
+
puts 'MEMORY PROFILING: Complex (Framed RMS)'
|
|
179
|
+
puts '=' * 50
|
|
180
|
+
|
|
181
|
+
# Profile Ruby Framed RMS
|
|
182
|
+
puts "\n[ Profiling Ruby Framed RMS... ]"
|
|
183
|
+
report_framed_ruby = MemoryProfiler.report { ruby_track.framed_rms }
|
|
184
|
+
report_framed_ruby.pretty_print(scale_bytes: false, color_output: false)
|
|
185
|
+
|
|
186
|
+
# Profile C++ Framed RMS
|
|
187
|
+
puts "\n[ Profiling C++ Framed RMS... ]"
|
|
188
|
+
report_framed_cpp = MemoryProfiler.report { cpp_track.framed_rms }
|
|
189
|
+
report_framed_cpp.pretty_print(scale_bytes: false, color_output: false)
|
|
File without changes
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
def parse_benchmark_log(filename)
|
|
4
|
+
results = {}
|
|
5
|
+
current_mem_test = nil
|
|
6
|
+
parsing_target = nil
|
|
7
|
+
|
|
8
|
+
File.foreach(filename) do |line|
|
|
9
|
+
# Parse Speedup (e.g., Read-Only (RMS) - Ruby: 71.3 i/s - 48.69x slower)
|
|
10
|
+
if match = line.match(%r{^\s*(.*?)\s*-\s*Ruby:\s+[\d.]+\s+i/s\s+-\s+([\d.]+)x\s+slower})
|
|
11
|
+
test_name = match[1].strip
|
|
12
|
+
results[test_name] ||= {}
|
|
13
|
+
results[test_name][:speedup] = match[2]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Parse Memory Profiling sections headers
|
|
17
|
+
if match = line.match(/^MEMORY PROFILING:\s*(.*)/)
|
|
18
|
+
current_mem_test = match[1].strip
|
|
19
|
+
results[current_mem_test] ||= {}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Determine if we are looking at Ruby or C++ memory
|
|
23
|
+
if line.include?('[ Profiling Ruby')
|
|
24
|
+
parsing_target = :ruby
|
|
25
|
+
elsif line.include?('[ Profiling C++')
|
|
26
|
+
parsing_target = :cpp
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Extract allocation numbers
|
|
30
|
+
if (match = line.match(/Total allocated:\s+(\d+)\s+bytes\s+\((\d+)\s+objects\)/)) && current_mem_test && parsing_target
|
|
31
|
+
results[current_mem_test][:"#{parsing_target}_bytes"] = format_number(match[1])
|
|
32
|
+
results[current_mem_test][:"#{parsing_target}_objs"] = format_number(match[2])
|
|
33
|
+
parsing_target = nil # reset after capturing
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
results
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def format_number(num_str)
|
|
41
|
+
# Adds commas to large numbers for readability
|
|
42
|
+
num_str.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def print_summary_table(results, log_filename)
|
|
46
|
+
puts '-' * 95
|
|
47
|
+
puts format('| %-25s | %-12s | %-22s | %-22s |', 'Benchmark', 'C++ Speedup', 'Ruby Allocation', 'C++ Allocation') # rubocop:disable Style/FormatStringToken
|
|
48
|
+
puts '-' * 95
|
|
49
|
+
|
|
50
|
+
results.each do |name, data|
|
|
51
|
+
speedup = data[:speedup] ? "#{data[:speedup]}x" : 'N/A'
|
|
52
|
+
ruby_alloc = data[:ruby_bytes] ? "#{data[:ruby_bytes]} B (#{data[:ruby_objs]} obj)" : 'N/A'
|
|
53
|
+
cpp_alloc = data[:cpp_bytes] ? "#{data[:cpp_bytes]} B (#{data[:cpp_objs]} obj)" : 'N/A'
|
|
54
|
+
|
|
55
|
+
puts format('| %-25s | %-12s | %-22s | %-22s |', name, speedup, ruby_alloc, cpp_alloc) # rubocop:disable Style/FormatStringToken
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
puts '-' * 95
|
|
59
|
+
puts "\nFull detailed log saved to: #{log_filename}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Execute only if run directly
|
|
63
|
+
if __FILE__ == $0
|
|
64
|
+
log_file = ARGV[0]
|
|
65
|
+
|
|
66
|
+
unless log_file && File.exist?(log_file)
|
|
67
|
+
puts 'Usage: ruby parse_log.rb <path_to_log_file>'
|
|
68
|
+
exit 1
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
results = parse_benchmark_log(log_file)
|
|
72
|
+
print_summary_table(results, log_file)
|
|
73
|
+
end
|
data/demo/demo_mbros.rb
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
require 'ruby_dsp'
|
|
2
|
+
|
|
3
|
+
track = RubyDSP::AudioTrack.new('', 1, 44_100)
|
|
4
|
+
|
|
5
|
+
# Frequencies for Mario (Hz)
|
|
6
|
+
e6 = 1318.51
|
|
7
|
+
c6 = 1046.50
|
|
8
|
+
g6 = 1567.98
|
|
9
|
+
g5 = 783.99
|
|
10
|
+
|
|
11
|
+
puts 'Sequencing Super Mario Bros Intro...'
|
|
12
|
+
|
|
13
|
+
track.add_wave!('square', e6, 0.1, 0.00)
|
|
14
|
+
.add_wave!('square', e6, 0.1, 0.15)
|
|
15
|
+
.add_wave!('square', e6, 0.1, 0.45)
|
|
16
|
+
.add_wave!('square', c6, 0.1, 0.75)
|
|
17
|
+
.add_wave!('square', e6, 0.1, 0.90)
|
|
18
|
+
.add_wave!('square', g6, 0.1, 1.20)
|
|
19
|
+
.add_wave!('square', g5, 0.1, 1.80)
|
|
20
|
+
.normalize!(-35.0)
|
|
21
|
+
.save_track('mario_intro.wav')
|
|
22
|
+
|
|
23
|
+
puts 'Done! Saved to mario_intro.wav'
|
data/demo/demo_tetris.rb
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
require 'ruby_dsp'
|
|
2
|
+
|
|
3
|
+
track = RubyDSP::AudioTrack.new('', 1, 44_100)
|
|
4
|
+
|
|
5
|
+
# Frequencies for Tetris (Hz)
|
|
6
|
+
e5 = 659.25
|
|
7
|
+
b4 = 493.88
|
|
8
|
+
c5 = 523.25
|
|
9
|
+
d5 = 587.33
|
|
10
|
+
a4 = 440.00
|
|
11
|
+
|
|
12
|
+
puts 'Sequencing Tetris (Korobeiniki)...'
|
|
13
|
+
|
|
14
|
+
track.add_wave!('sawtooth', e5, 0.30, 0.0)
|
|
15
|
+
.add_wave!('sawtooth', b4, 0.15, 0.4)
|
|
16
|
+
.add_wave!('sawtooth', c5, 0.15, 0.6)
|
|
17
|
+
.add_wave!('sawtooth', d5, 0.30, 0.8)
|
|
18
|
+
.add_wave!('sawtooth', c5, 0.15, 1.2)
|
|
19
|
+
.add_wave!('sawtooth', b4, 0.15, 1.4)
|
|
20
|
+
.add_wave!('sawtooth', a4, 0.30, 1.6)
|
|
21
|
+
.add_wave!('sawtooth', a4, 0.15, 2.0)
|
|
22
|
+
.add_wave!('sawtooth', c5, 0.15, 2.2)
|
|
23
|
+
.add_wave!('sawtooth', e5, 0.30, 2.4)
|
|
24
|
+
.normalize!(-35.0)
|
|
25
|
+
.save_track('tetris_theme.wav')
|
|
26
|
+
|
|
27
|
+
puts 'Done! Saved to tetris_theme.wav'
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
require 'ruby_dsp'
|
|
2
|
+
|
|
3
|
+
track = RubyDSP::AudioTrack.new('', 1, 44_100)
|
|
4
|
+
|
|
5
|
+
# frequencies for Twinkle (Hz)
|
|
6
|
+
c4 = 261.63
|
|
7
|
+
d4 = 293.66
|
|
8
|
+
e4 = 329.63
|
|
9
|
+
f4 = 349.23
|
|
10
|
+
g4 = 392.00
|
|
11
|
+
a4 = 440.00
|
|
12
|
+
|
|
13
|
+
puts 'Sequencing 8-bit Twinkle Twinkle...'
|
|
14
|
+
|
|
15
|
+
track.add_wave!('square', c4, 0.4, 0.0)
|
|
16
|
+
.add_wave!('square', c4, 0.4, 0.5)
|
|
17
|
+
.add_wave!('square', g4, 0.4, 1.0)
|
|
18
|
+
.add_wave!('square', g4, 0.4, 1.5)
|
|
19
|
+
.add_wave!('square', a4, 0.4, 2.0)
|
|
20
|
+
.add_wave!('square', a4, 0.4, 2.5)
|
|
21
|
+
.add_wave!('square', g4, 0.9, 3.0)
|
|
22
|
+
.add_wave!('square', f4, 0.4, 4.0)
|
|
23
|
+
.add_wave!('square', f4, 0.4, 4.5)
|
|
24
|
+
.add_wave!('square', e4, 0.4, 5.0)
|
|
25
|
+
.add_wave!('square', e4, 0.4, 5.5)
|
|
26
|
+
.add_wave!('square', d4, 0.4, 6.0)
|
|
27
|
+
.add_wave!('square', d4, 0.4, 6.5)
|
|
28
|
+
.add_wave!('square', c4, 0.9, 7.0)
|
|
29
|
+
.normalize!(-35.0)
|
|
30
|
+
.fade_out!(0.5)
|
|
31
|
+
.save_track('twinkle_twinkle.wav')
|
|
32
|
+
|
|
33
|
+
puts 'Done! Saved to twinkle_8bit.wav'
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
data/ext/ruby_dsp/ruby_dsp.cpp
CHANGED
data/lib/ruby_dsp/version.rb
CHANGED
data/ruby_dsp.gemspec
CHANGED
|
@@ -21,6 +21,9 @@ Gem::Specification.new do |s|
|
|
|
21
21
|
|
|
22
22
|
s.add_dependency 'rice', '~> 4.11.2'
|
|
23
23
|
|
|
24
|
+
s.add_development_dependency 'benchmark', '~> 0.5.0'
|
|
25
|
+
s.add_development_dependency 'benchmark-ips', '~> 2.14'
|
|
26
|
+
s.add_development_dependency 'memory_profiler', '~> 1.1'
|
|
24
27
|
s.add_development_dependency 'minitest', '~> 6.0'
|
|
25
28
|
s.add_development_dependency 'rack', '~> 3.2'
|
|
26
29
|
s.add_development_dependency 'rackup', '~> 2.3'
|
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.
|
|
4
|
+
version: 0.0.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Radek C.
|
|
@@ -23,6 +23,48 @@ dependencies:
|
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: 4.11.2
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: benchmark
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 0.5.0
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: 0.5.0
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: benchmark-ips
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '2.14'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '2.14'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: memory_profiler
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '1.1'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '1.1'
|
|
26
68
|
- !ruby/object:Gem::Dependency
|
|
27
69
|
name: minitest
|
|
28
70
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -150,6 +192,15 @@ files:
|
|
|
150
192
|
- Gemfile
|
|
151
193
|
- README.md
|
|
152
194
|
- Rakefile
|
|
195
|
+
- benchmark/bench.rb
|
|
196
|
+
- benchmark/logs/.gitkeep
|
|
197
|
+
- benchmark/parse_log.rb
|
|
198
|
+
- demo/demo_mbros.rb
|
|
199
|
+
- demo/demo_tetris.rb
|
|
200
|
+
- demo/demo_twinkle.rb
|
|
201
|
+
- demo/mario_intro.mp4
|
|
202
|
+
- demo/tetris_theme.mp4
|
|
203
|
+
- demo/twinkle_twinkle.mp4
|
|
153
204
|
- ext/ruby_dsp/extconf.rb
|
|
154
205
|
- ext/ruby_dsp/ruby_dsp.cpp
|
|
155
206
|
- ext/ruby_dsp/vendor/miniaudio.h
|