Skip to content

A library of simple Ruby tools for processing sound.

License

Notifications You must be signed in to change notification settings

mike-bourgeous/mb-sound

Repository files navigation

mb-sound

Tests

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.

Quick start

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

Examples

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.

Generating tones

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)

Noise

# Mono
play noise

# Stereo
play [noise, noise]

# Brown(ish)
play noise.at(2).filter(30.hz.lowpass1p).softclip

Simple AM tones

play 123.hz * 369.hz

Simple FM tones

Frequency modulation is also possible:

play 123.hz.fm(369.hz.at(1000))

Calculating wavelength and frequency

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

Filtering sounds

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

Playing a sound file

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))

Loading a sound file into memory

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

Plotting sounds

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

Visualizing realtime sound

loopback(plot: { spectrum: true })

Working with MIDI

Look under the lib/mb/sound/midi/ directory, or refer to the example scripts below.

bin/midi_info.rb

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

bin/midi_roll.rb

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  ┊                                                   ┗━━━━━━━━━━━━━━━━━┛

bin/midi_cc_chart.rb

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 |     |     |     |     |     |     |     |     |     |

bin/midi_note_chart.rb

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   |     |     |     |     |     |     |     |     |     |     |     |

bin/ep2_syn.rb

This is the synthesizer I wrote for episode 2 of my Code, Sound & Surround video series.

bin/ep2_syn.rb

Installation and usage

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.

Using the project by itself

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.

Using the project as a dependency

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'

Testing

You can run the integrated test suite with rspec.

Contributing

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.

License

This project is released under a 2-clause BSD license. See the LICENSE file.

See also

Dependencies

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

References

There are lots of excellent resources out there for learning sound and signal processing:

About

A library of simple Ruby tools for processing sound.

Topics

Resources

License

Stars

Watchers

Forks