A library of simple Ruby tools for processing sound, and a DSL for building signal processing chains. This is a companion library to an educational video series I'm making about sound.
You'll find functions for loading and saving audio files, playing and recording sound in realtime (on Linux, and only for really simple algorithms), generating sounds with subtractive, FM, and AM synthesis, and plotting sounds.
pry.mp4
This is written in Ruby for speed of prototyping, convenience of the Ruby command line, and for the sake of novelty. Another reason is that, if we can write sound algorithms that work fast enough in Ruby, then they'll definitely be fast enough when ported to e.g. C, Rust, GLSL, etc.
More information may be added here as the video series progresses. For now, the most interesting things you'll want to know about using this library are installing the dependencies, and launching the interactive command line.
You might also be interested in mb-math, mb-geometry, and mb-util.
Clone the repo, follow the installation instructions
below, then run bin/sound.rb
.
Try these first, then look at the examples section below:
# Mixing different sounds together
play file_input('sounds/synth0.flac') * 120.hz.fm(360.hz.at(1000))
# Badly tuned radio/robot effect
play (input * 400.hz * 10).softclip.filter(150.hz.highpass(quality: 4))
# Old telephone
play (input * 400.hz.at(0.5..1.0) * 10).softclip(0, 1).filter(250.hz.highpass(quality: 4)).filter(3500.hz.lowpass(quality: 2)).softclip
# Simple musical rhythm
play (
(
(
D4.triangle.forever *
4.hz.ramp.at(1..0).filter(100.hz.lowpass) *
0.125.hz.triangle
) + (
(D2.triangle.forever + noise.at(-46.db)) *
2.hz.ramp.at(1..0).filter(100.hz.lowpass)
) + (
D1.square.forever.filter(1000.hz.lowpass(quality: 4)) *
0.5.hz.ramp.at(1..0).filter(100.hz.lowpass) *
(1/32.0).hz.triangle
)
) * 4
).softclip(0, 0.8)
# Simulating a PWM oscillator with a clipped triangle wave
square_lfo = 0.6.hz.square.at(0.5..6.5).smooth(seconds: 0.15)
play 0.5 * (400.hz.triangle.at(10) + square_lfo).clip(-0.2, 0.2).filter(50.hz.highpass)
# Saving a tone to an audio file
write('/tmp/ramp.flac', D3.ramp * adsr(0.2, 0.2, 0.1, 0.4), overwrite: true)
play '/tmp/ramp.flac'
# Some hi-hat rhythms
play 1000.hz.sine.noise.at(-30.db).filter(7000.hz.highpass(quality: 10)).filter(12345.hz.lowpass(quality: 4)) * (1.25.hz.ramp.with_phase(Math::PI).at(0..-60).db + 2.5.hz.ramp.at(-10..-70).db)
play 1000.hz.sine.noise.at(-30.db).filter(7000.hz.highpass(quality: 10)).filter(12345.hz.lowpass(quality: 4)) * (5.hz.ramp.with_phase(Math::PI).at(0..-60).db + 5.hz.ramp.at(-10..-70).db).forever
# Heavily distorted synth kick
play (2.5.hz.ramp.at(1.85) ** 13).filter(10.hz.highpass).softclip(0.1, 0.6).filter(cutoff: 2.5.hz.ramp.at(1..0) ** 10 * 0.2.hz.sine.at(120..300) + 40, quality: 14).filter(40.hz.highpass).softclip.forever
These examples can be run in the bin/sound.rb
interactive environment. There
are other examples in the scripts under the bin/
directory, such as an FM
synthesizer in bin/fm_synth.rb
and a flanger effect in bin/flanger.rb
.
5.times do
play 100.hz.triangle.at(-20.db).for(0.25)
play 133.hz.triangle.at(-20.db).for(0.25)
play 150.hz.triangle.at(-20.db).for(0.25)
play 100.hz.triangle.at(-20.db).for(0.25)
play 200.hz.ramp.at(-23.db).for(1.6)
end
You can play different tones in each channel:
# Stereo octave
play [200.hz, 100.hz]
# Binaural beat
play [100.hz, 103.hz]
# Surround sound chord
play [100.hz, 200.hz, 300.hz, 400.hz, 500.hz, 600.hz, 250.hz, 333.hz].map(&:triangle)
# Mono
play noise
# Stereo
play [noise, noise]
# Brown(ish)
play noise.at(2).filter(30.hz.lowpass1p).softclip
play 123.hz * 369.hz
Frequency modulation is also possible:
play 123.hz.fm(369.hz.at(1000))
There are DSL methods for working with distances and wavelengths:
1.hz.wavelength
# => 343 meters
343.meters.hz
# => #<MB::Sound::Tone:0x000055f66a23a2b0
# @amplitude=0.1,
# @duration=5.0,
# @frequency=1.0,
# @oscillator=nil,
# @rate=48000,
# @wave_type=:sine,
# @wavelength=343.0 meters>
You can convert between feet and meters:
1000.hz.wavelength.feet
# => 1.1253280839895015 feet
1.foot.meters
# => 0.30479999999999996 meters
Filters delay and/or change the volume of different frequencies.
tone = 432.hz.ramp
filtered = 432.hz.ramp.filter(1500.hz.lowpass(quality: 0.25))
# Compare the unfiltered and filtered tones
plot [tone, filtered], samples: 48000*3/432.0
play filtered
+---------------------------------------------------------------------------------+
0.08 |-+ ***+* + +**** + + **** + +-|
0.06 |-+ *** * *** * *** * 0 *******-|
0.04 |-+ *** * *** * *** * +-|
0.02 |-*** * **** * *** * +-|
0 |** * *** * *** * +-|
| * *** * *** * *** |
-0.02 |-+ * *** * **** * *** +-|
-0.04 |-+ * *** * *** * *** +-|
-0.06 |-+ * *** * *** * *** +-|
-0.08 |-+ +**** + + **** + + * ***+ +-|
+---------------------------------------------------------------------------------+
0 50 100 150 200 250 300 350
+---------------------------------------------------------------------------------+
0.08 |-+ + + + + + + +-|
0.06 |-+ ** ** ** 1 *******-|
| *** * *** * *** * |
0.04 |-+ *** ** *** ** *** ** +-|
0.02 |-+ **** * **** * *** * +-|
0 |**** ** *** ** *** ** +-|
-0.02 |-+ *** **** *** **** *** +-|
-0.04 |-+ ********* ********* ********* +-|
| |
-0.06 |-+ +-|
-0.08 |-+ + + + + + + +-|
+---------------------------------------------------------------------------------+
0 50 100 150 200 250 300 350
play 'sounds/sine/sine_100_1s_mono.flac', gain: -6.db
You can also plot the spectrum of a playing sound instead of its waveform:
play 'sounds/sine/log_sweep_20_20k.flac', spectrum: true
You can filter the sound as well:
play file_input('sounds/synth0.flac').filter(1500.hz.lowpass(quality: 8))
data = read 'sounds/sine/sine_100_1s_mono.flac'
# => [Numo::DFloat#shape=[48000]
# [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1.19209e-07, ...]]
play data, rate: 48000
plot 100.hz.ramp
+-------------------------------------------------------------------+
0.08 |-+ + **** + + + + ***** + + +-|
| **** * *** * 0 ******* |
0.06 |-+ *** * *** * +-|
0.04 |-+ *** * **** * +-|
| **** * *** * |
0.02 |*** * *** * +-|
0 |*+ * **** * **+-|
-0.02 |-+ * *** * *** +-|
| * *** * **** |
-0.04 |-+ * **** * *** +-|
-0.06 |-+ * *** * *** +-|
| * *** * **** |
-0.08 |-+ + + * *** + + + +**** + + +-|
-0.1 +-------------------------------------------------------------------+
0 100 200 300 400 500 600 700 800 900 1000
spectrum 100.hz.ramp
# or
plot 100.hz.ramp, spectrum: true
+---------------------------------------------------------------------+
| ** + + |
-30 |-+ * * * * 0 *******-|
| * * * * * * |
-40 |-+ * * ** * * * * * +-|
| * * * * ** * * * ** **** |
| * * * * * * ** * * ** ********** |
-50 |-+ * * * * * * ** * * ** *************** +-|
| * * * * * * ** * * ** ******************** |
-60 |-+* * * * * * * * ***** ************************** +-|
| * * * * * * * * * * *** ******************************* |
| * * * ** ** * * * *** ***********************************|
-70 |*+ * * * * * * * *** ***********************************|
|* * * * *+* * *** ***********************************|
-80 +---------------------------------------------------------------------+
1 10 100
loopback(plot: { spectrum: true })
Look under the lib/mb/sound/midi/
directory, or refer to the example scripts
below.
This script displays information about a MIDI file, including the song title, track names, and number of events on each track. Uses the midilib gem for parsing MIDI files.
bin/midi_info.rb spec/test_data/midi.mid
midi.mid: Unnamed
-----------------
# | Name | Inst. | Ch. mask | Event ch. | Events | Notes
---+---------+-------+----------+-----------+--------+-------
0 | Unnamed | | [] | [] | 3 | 0
1 | Unnamed | | [0] | [0] | 32 | 26
This draws a MIDI piano roll to the terminal, with each channel getting its own color.
bin/midi_roll.rb --help
bin/midi_roll.rb -r 15 -c 80 spec/test_data/midi.mid
spec/test_data/midi.mid -- 0.0..6.857136/6.86s
82 A♯5 ┊
81 A5 ┊
80 G♯5 ┗━━━━━━━━━━━━━━━━┛ ┗━━━━━━━━━━━━━━━━┗━━━━━━━━━━━━━━━━━┛
79 G5 ┊ ┗━━━━━━━━━━━━━━━━━┛
78 F♯5 ┊
77 F5 ┊ ┗━━━━━━━━━━━━━━━━┛
76 E5 ┊
75 D♯5 ┗━━━━━━━━━━━━━━━━┗━━━━━━━━━━━━━━━━━┛ ┗━━━━━━━━━━━━━━━━━┛
74 D5 ┊
73 C♯5 ┊ ┗━━━━━━━━━━━━━━━━┛
72 C5 ┗━━━━━━━━━━━━━━━━┗━━━━━━━━━━━━━━━━━┛ ┗━━━━━━━━━━━━━━━━━┛
71 B4 ┊
70 A♯4 ┊
69 A4 ┊
68 G♯4 ┊ ┗━━━━━━━━━━━━━━━━━┛
This displays a table with MIDI Control Change (CC) values, either from a MIDI input or a MIDI file, in real time.
bin/midi_cc_chart.rb spec/test_data/midi.mid
CCs | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----
0 | 16 | | | | | | 12 | | |
-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----
10 | | | | | | | | | |
-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----
20 | | | | | | | | | |
-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----
30 | | | 0 | | | | | | |
-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----
40 | | | | | | | | | |
-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----
50 | | | | | | | | | |
-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----
60 | | | | | | | | | |
-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----
70 | | | | | | | | | |
-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----
80 | | | | | | | | | |
-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----
90 | | | | | | | | | |
-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----
100 | | | | | | | | | |
-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----
110 | | | | | | | | | |
-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----
120 | | | | | | | | | |
This displays the attack and release velocities of MIDI Note On and Note Off events in a grid, either from a MIDI input or a MIDI file, in real time.
bin/midi_note_chart.rb spec/test_data/midi.mid
### | C | C# | D | D# | E | F | F# | G | G# | A | A# | B
-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----
-1 | | | | | | | | | | | |
-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----
0 | | | | | | | | | | | |
-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----
1 | | | | | | | | | | | |
-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----
2 | | | | | | | | | | | |
-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----
3 | | | | | | | | | | | |
-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----
4 | | | | | | | | | | | |
-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----
5 | 0 | 118 | | 0 | | 33 | | 0 | 31 | | |
-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----
6 | | | | | | | | | | | |
-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----
7 | | | | | | | | | | | |
-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----
8 | | | | | | | | | | | |
-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----
9 | | | | | | | | | | | |
This is the synthesizer I wrote for episode 2 of my Code, Sound & Surround video series.
bin/ep2_syn.rb
You can either tinker within this project, or use it as a Git-sourced Gem in your own projects.
There are some base packages you'll need first:
# Debian-/Ubuntu-based Linux (macOS/Arch/CentOS will differ)
sudo apt-get install ffmpeg gnuplot-qt
# macOS (with Homebrew)
brew install ffmpeg gnuplot
Then you'll want to install Ruby 2.7.2 or newer.
If you don't already have a recent version of Ruby installed, and a Ruby version manager of your choosing, I highly recommend using RVM. You can find installation instructions for RVM at https://rvm.io.
After getting RVM installed, you'll want to clone this repository, install Ruby, and install the Gems needed by this code.
I also recommend making a separate projects directory just for this video series.
This assumes basic familiarity with the Linux/macOS/WSL command line, or enough independent knowledge to make this work on your operating system of choice. I'll provide an overly detailed Linux example here:
# Make a project directory (substitute your own preferred paths)
cd ~/projects
mkdir sound_code_series
cd sound_code_series
# Install Ruby
# (disable-binary is needed on Ubuntu 20.04 to fix "/usr/bin/mkdir not found"
# error in the binary package of 2.7.2)
rvm install --disable-binary 2.7.2
# Clone the repo
git clone [email protected]:mike-bourgeous/mb-sound.git
cd mb-sound
# Install Gem dependencies
cd mb-sound
gem install bundler
bundle install
# Compile the mb-sound C extensions
rake compile
Now that everything's installed, you are ready to start playing with sound:
# Launch the interactive command line
bin/sound.rb
See the Examples section for some things to try.
If you're already familiar with Ruby and Gems, then you can add this repo as a dependency to a new project's Gemfile.
# your-project/Gemfile
gem 'mb-sound', git: 'https://github.com/mike-bourgeous/mb-sound.git'
# Also specify Git location for other mb-* dependencies
gem 'mb-util', git: 'https://github.com/mike-bourgeous/mb-util.git'
gem 'mb-math', git: 'https://github.com/mike-bourgeous/mb-math.git'
You can run the integrated test suite with rspec
.
Since this library is meant to accompany a video series, most new features will be targeted at what's covered in episodes as they are released. If you think of something cool to add that relates to the video series, then please open a pull request.
Pull requests are also welcome if you want to add or improve support for new platforms.
This project is released under a 2-clause BSD license. See the LICENSE file.
This code uses some really cool other projects either directly or indirectly:
- FFMPEG
- Numo::NArray
- Numo::Pocketfft
- Pry interactive console for Ruby
- GNUplot
- The MIDI Nibbler gem
There are lots of excellent resources out there for learning sound and signal processing:
- I've created a playlist with some cool videos by others
- Circles, sines, and signals is a great interactive demonstration of waves and Fourier transforms
- Online books by Julius O. Smith (I recommend buying the print versions)