A real-time audio DSP library for Ruby based on ffi-portaudio, designed to create synthesizers, apply filters, and generate audio in real-time with a simple, expressive API.
- Real-time output with ffi-portaudio
- Native C extension for glitch-free audio
- Output to speaker or wave file
- DualRPMOscillator: Antialiased hard sync, through-zero FM, and PWM
- Voice: High-level subtractive synth wrapper with flexible parts
- MIDI input via portmidi ? (untested)
- Refinements for scoped DSP extensions
- 4x oversampling with anti-aliasing
- Audio-rate filter modulation
require 'radspberry'
include DSP
# Play a note with Voice preset
voice = Voice.acid
Speaker.play(voice, volume: 0.4)
# Play :a2 for 0.5 seconds (blocks)
voice.play(:a2, 0.5)
Speaker.stopSee /examples for more. Notable scripts to run:
examples/synth_examples.rb: A comprehensive tour of the API, including patterns, arpeggiators, timing helpers, and modulation.examples/naive_rpm_sync_demo.rb: An educational comparison demonstrating the audible difference between naive (aliasing) and proper (antialiased) oscillator hard sync.examples/dual_rpm_showcase.rb: A deep dive into the advanced features ofDualRPMOscillator, showcasing through-zero FM, PWM, and Kaiser window morphing.
Use Ruby symbols for musical notes. Most methods expecting a frequency (like Voice#freq=) accept these directly.
:c4.freq # => 261.63 Hz
:a4.midi # => 69
:c4.major # => [:c4, :e4, :g4]
:a3.minor7 # => [:a3, :c4, :e4, :g4]
:c4 + 7 # => :g4 (transpose up 7 semitones)
# Scales
:c3.scale(:major) # => [:c3, :d3, :e3, :f3, :g3, :a3, :b3, :c4]
:c3.scale(:blues, octaves: 2) # Two octaves of blues scale
:c3.scale(:dorian) # Modal scalesAvailable scales: major, minor, harmonic_minor, melodic_minor, dorian, phrygian, lydian, mixolydian, locrian, pentatonic, minor_pentatonic, blues, chromatic, whole_tone.
Voice is a flexible subtractive synthesizer chain (Oscillator -> Filter -> Amplifier).
# Presets - optionally pass a note to start immediately
v = Voice.acid(:a2) # TB-303 style acid bass
v = Voice.pad(:c3) # Lush pad with slow attack
v = Voice.pluck(:e4) # Plucky percussive sound
v = Voice.lead(:g4) # Monophonic lead
# Control voices
v.play(:c4) # Trigger note (on)
v.play(:c4, 0.5) # Trigger note for 0.5 seconds
v.stop # Release note (off)
# Custom Voice Construction
# Pass classes (auto-instantiated) or specific instances
v = Voice.new(
osc: DualRPMOscillator,
filter: AudioRateSVF,
amp_env: Env.adsr(attack: 0.1),
filter_env: Env.perc
)# Parameter aliases for clean API
v.cutoff = 2000 # Filter base frequency
v.res = 0.8 # Filter resonance
v.attack = 0.01 # Amp envelope attack
v.decay = 0.2 # Amp envelope decay
v.sustain = 0.6 # Amp envelope sustain level
v.release = 0.3 # Amp envelope release
# Bulk parameter update
v.set(cutoff: 1500, res: 0.5, attack: 0.05)The DualRPMOscillator is a feature-rich oscillator providing antialiased hard sync, PWM, and FM using Recursive Phase Modulation (RPM) and Kaiser windowing.
osc = DualRPMOscillator.new(:c3.freq)
# Antialiased Hard Sync & PWM
osc.sync_ratio = 2.5 # Sync frequency ratio
osc.window_alpha = 4.0 # Window smoothness (higher = smoother, less aliasing)
osc.duty = 0.5 # Pulse width (or phase offset for dual slaves)
# RPM Core Parameters
osc.beta = 1.5 # Recursive modulation amount (harmonic richness)
osc.morph = 0.5 # Morph between Saw-like (0.0) and Square-like (1.0)
# Modulation Example
# Modulate sync ratio for "laser" sweeps
lfo = Phasor.new(0.5)
osc_mod = osc.modulate(:sync_ratio, lfo, range: 1.0..8.0)Env.perc # Quick percussive hit
Env.pluck # Plucked string decay
Env.pad # Slow pad envelope
Env.adsr(attack: 0.1, decay: 0.2, sustain: 0.6, release: 0.4)
Env.ad(attack: 0.01, decay: 0.5)Time values default to seconds, but helpers exist for beat-based logic:
Clock.bpm = 140
sleep 1.beat # Sleep for one beat (0.429s at 140 BPM)
sleep 0.5.beats # Half beat
sleep 1.bar # One bar (4 beats)
sleep 2.bars # Two barsDeclaratively modulate any parameter with an LFO or other source:
filter = ButterLP.new(1000)
lfo = Phasor.new(5) # 5Hz LFO
# Range-based modulation
filter = filter.modulate(:freq, lfo, range: 200..4000)
# Block-based (custom curve)
filter = filter.modulate(:q, lfo) { |v| 0.5 + v * 10 }
# Chain multiple modulations
filter = ButterLP.new(1000)
.modulate(:freq, lfo1, range: 200..4000)
.modulate(:q, lfo2, range: 0.5..10)# Play any generator
Speaker.play(voice, volume: 0.4)
# Play with duration (stops automatically)
Speaker.play(voice, volume: 0.4, duration: 2.0)
# Stop playback
Speaker.stop
# Check status
Speaker.playing? # => true/false# Install portaudio
brew install portaudio # macOS
# apt install portaudio19-dev # Linux
# Clone the repository
git clone git@github.com:dfl/radspberry.git
cd radspberry
# Install dependencies
bundle install
# Build native extension (optional, for NativeSpeaker)
cd ext/radspberry_audio && ruby extconf.rb && make
mkdir -p ../../lib/radspberry_audio
cp radspberry_audio.bundle ../../lib/radspberry_audio/
# Run a demo
ruby examples/buffered_demo.rbThe default Speaker uses ffi-portaudio's callback mode, where PortAudio's native audio thread calls into Ruby. This requires acquiring Ruby's Global VM Lock (GVL), which means heavy Ruby work (computation, GC) can cause audio glitches.
For stable audio during heavy Ruby work, use NativeSpeaker which uses a C extension:
NativeSpeaker.new(SuperSaw.new(110), volume: 0.3)
# Audio stays smooth even during heavy computation
10.times { heavy_computation }
NativeSpeaker.stopThe C callback reads from a lock-free ring buffer without touching Ruby or the GVL. A Ruby producer thread fills the buffer in the background.
Build the extension:
cd ext/radspberry_audio
ruby extconf.rb
make
cp radspberry_audio.bundle ../../lib/radspberry_audio/The AudioRateSVF is a TPT/Cytomic-style state variable filter optimized for audio-rate modulation:
# Create filter with saturation
svf = AudioRateSVF.new(freq: 1000, q: 4.0, kind: :low, drive: 12.0)
# Audio-rate frequency modulation (efficient - uses fast tan approximation)
lfo = Phasor.new(5) # 5Hz LFO
noise = Noise.new
loop do
mod_freq = 500 + lfo.tick * 2000 # Modulate 500-2500Hz
output = svf.tick_with_mod(noise.tick, mod_freq)
end
# Filter modes: :low, :band, :high, :notch
# 4-pole mode (24dB/oct):
svf.four_pole = trueOversampling reduces aliasing from nonlinear processing (saturation, distortion). Uses a 12th-order elliptic anti-aliasing filter.
svf = AudioRateSVF.new(freq: 2000, drive: 18.0)
oversampled = DSP.oversample(svf)
chain = noise >> oversampledchain = DSP.oversampled do
osc = Phasor.new(440)
filter = AudioRateSVF.new(freq: 2000, drive: 12.0)
osc >> filter
end
# Everything inside runs at 176.4kHz, decimated to 44.1kHz at output
chain.to_wav(2, filename: "smooth_saturation.wav")The chain approach is cleaner because all components use the correct sample rate automatically, and there's only one decimation point at output.
By default, radspberry adds helper methods globally to Array, Vector, and Module. If you prefer explicit, lexically-scoped extensions, use refinements instead:
require 'radspberry'
using DSP::Refinements
# These methods only work in this file:
[1, 2, 3].to_v # => Vector[1, 2, 3]
Vector.zeros(4) # => Vector[0.0, 0.0, 0.0, 0.0]
[osc1, osc2].tick_sum # Sum tick values from array of oscillatorsRefinements are activated with using and are scoped to the current file—they won't leak into other parts of your codebase.
- Ruby >= 2.7
- portaudio library
- ffi-portaudio gem
- CHANGELOG - Version history and release notes
- COMPOSITION - Function composition patterns
Some directions worth exploring:
Use Ruby's lightweight coroutines for stateful, event-driven generators:
env = Fiber.new do
100.times { |i| Fiber.yield(i / 100.0) } # attack
loop { Fiber.yield(1.0) } # sustain until released
endTreat chains as inspectable/optimizable structures before execution:
graph = osc >> gain(0.5) >> gain(0.8) >> filter
graph.optimize! # => osc >> gain(0.4) >> filter
graph.compile! # => generate specialized native codeFor bounce-to-disk, split work across cores:
samples = synth.parallel_render(seconds: 60, cores: 8)MIT - Copyright (c) 2012-2025 David Lowenfels