RSound: An Adequate Sound Engine for Racket
A note about volume: be careful not to damage your hearing, please. To take a simple example, the sine-wave function generates a sine wave with amplitude 1.0. That translates into the loudest possible sine wave that can be represented. So please set your volume low, and be careful with the headphones. Maybe there should be a parameter that controls the clipping volume. Hmm.
1 A NOTE ABOUT WINDOWS
Windows is a bit of a pain for developers. If you’re having trouble hearing sounds under windows (high latency, or "Invalid Device" errors), try running diagnose-sound-playing.
procedure
2 Sound Control
These procedures start and stop playing sounds and loops.
3 Sound I/O
These procedures read and write rsounds from/to disk.
The RSound library reads and writes WAV files only; this means fewer FFI dependencies (the reading & writing is done in Racket), and works on all platforms.
procedure
path : path-string?
It currently has lots of restrictions (it insists on 16-bit PCM encoding, for instance), but deals with a number of common bizarre conventions that certain WAV files have (PAD chunks, extra blank bytes at the end of the fmt chunk, etc.), and tries to fail relatively gracefully on files it can’t handle.
Reading in a large sound can result in a very large value (~10 Megabytes per minute); for larger sounds, consider reading in only a part of the file, using rs-read/clip.
procedure
(rs-read/clip path start finish) → rsound?
path : path-string? start : nonnegative-integer? finish : nonnegative-integer?
It currently has lots of restrictions (it insists on 16-bit PCM encoding, for instance), but deals with a number of common bizarre conventions that certain WAV files have (PAD chunks, extra blank bytes at the end of the fmt chunk, etc.), and tries to fail relatively gracefully on files it can’t handle.
procedure
(rs-read-frames path) → nonnegative-integer?
path : path-string?
The file must be encoded as a WAV file readable with rsound-read.
procedure
(rs-read-sample-rate path) → number?
path : path-string?
The file must be encoded as a WAV file readable with rs-read.
procedure
rsound : rsound? path : path-string?
4 Rsound Manipulation
These procedures allow the creation, analysis, and manipulation of rsounds.
struct
(struct rsound (data start end sample-rate) #:extra-constructor-name make-rsound) data : s16vector? start : nonnegative-number? end : nonnegative-number? sample-rate : nonnegative-number?
This procedure is necessary because s16vectors don’t natively support equal?.
procedure
(rs-ith/left rsound frame) → nonnegative-integer?
rsound : rsound? frame : nonnegative-integer?
procedure
(rs-ith/right rsound frame) → nonnegative-integer?
rsound : rsound? frame : nonnegative-integer?
procedure
rsound : rsound? start : nonnegative-integer? finish : nonnegative-integer?
procedure
(rs-append* rsounds) → rsound?
rsounds : (listof rsound?)
procedure
(rs-overlay rsound-1 rsound-2) → rsound?
rsound-1 : rsound? rsound-2 : rsound?
procedure
(rs-overlay* rsounds) → rsound?
rsounds : (listof rsound?)
So, suppose we have two rsounds: one called ’a’, of length 20000, and one called ’b’, of length 10000. Evaluating
(rs-overlay* (list (list a 5000) (list b 0) (list b 11000)))
... would produce a sound of 21000 frames, where each instance of ’b’ overlaps with the central instance of ’a’.
5 Signals and Networks
For signal processing, RSound adopts a dataflow-like paradigm. Networks represent interconnected signal-processing nodes, and produce streams of values. They can be connected together using a number of primitives, including the network syntactic form. Networks that have no inputs are called signals.
Here’s a trivial signal:
(network () [out 3])
This is the signal that always produces 3.
Here’s another one, that counts upward:
(define counter/sig (network () [counter (+ 1 (prev counter 0))]))
The prev form is special, and is used to refer to the prior value of the signal component.
Note that since we’re adding one immediately, this counter starts at 1.
Here’s another example, that adds together two sine waves, at 34 Hz and 46 Hz, assuming a sample rate of 44.1KHz:
(define sum-of-sines (network () [a (sine-wave 34)] [b (sine-wave 46)] [out (+ a b)]))
a network can have many clauses; each clause contains a name and a right-hand-side.
a right-hand-side must be a constant, or an application, either of a primitive function or of a network.
the last clause is used as the output, regardless of its name.
clauses can produce multiple values; in this case, the name is replaced by a parenthesized list.
In order to use a signal with signal-play, it should produce a real number in the range -1.0 to 1.0.
Here’s an example that uses one sine-wave (often called an "LFO") to control the pitch of another one:
(define vibrato-tone (network () [lfo (sine-wave 2)] [sin (sine-wave (+ 400 (* 50 lfo)))] [out (* 0.1 sin)])) (signal-play vibrato-tone) (sleep 5) (stop)
There are many built-in signals. Note that these are documented as though they were procedures, but they’re not; they can be used in a procedure-like way in network clauses. Otherwise, they will behave as opaque values; you can pass them to various signal functions, etc.
Also note that all of these assume a fixed sample rate of 44.1 KHz.
signal
(sawtooth-wave frequency) → real?
frequency : nonnegative-number?
signal
(square-wave frequency) → real?
frequency : nonnegative-number?
Also note that since this is a simple 1/-1 square wave, it’s got horrible aliasing all over the spectrum.
signal
(pulse-wave duty-cycle frequency) → real?
duty-cycle : real? frequency : nonnegative-number?
In order to listen to them, you can transform them into rsounds, or play them directly:
procedure
(signal->rsound frames signal) → rsound?
frames : nonnegative-integer? signal : signal?
Here’s an example of using it:
(define sig1 (network () [a (sine-wave 560)] [out (* 0.1 a)])) (define r (signal->rsound 44100 sig1)) (play r)
procedure
(signals->rsound frames left-sig right-sig) → rsound?
frames : nonnegative-integer? left-sig : signal? right-sig : signal?
with integers from 0 up to frames-1. The result should be an inexact number in the range -1.0 to 1.0. Values outside this range are clipped.
procedure
(signal-play signal) → void?
signal : signal?
There are several functions that produce signals.
procedure
(indexed-signal time->amplitude) → signal?
time->amplitude : procedure?
There are also a number of functions that combine existing signals, called "signal combinators":
We can turn an rsound back into a signal, using rsound->signal:
procedure
(rsound->signal/left rsound) → signal?
rsound : rsound?
procedure
(rsound->signal/right rsound) → signal?
rsound : rsound?
procedure
(thresh/signal threshold signal) → signal?
threshold : real-number? signal : signal?
procedure
(clip&volume volume signal) → signal?
volume : real-number? signal : signal?
Where should these go?
procedure
(thresh threshold input) → real-number?
threshold : real-number? input : real-number?
Finally, here’s a predicate. This could be a full-on contract, but I’m afraid of the overhead.
6 Visualizing Rsounds
(require (planet clements/rsound:4:=3/draw)) |
procedure
(rs-draw rsound #:title title [ #:width width #:height height]) → void? rsound : rsound? title : string? width : nonnegative-integer? = 800 height : nonnegative-integer? = 200
procedure
(rsound-fft-draw rsound #:zoom-freq zoom-freq #:title title [ #:width width #:height height]) → void? rsound : rsound? zoom-freq : nonnegative-real? title : string? width : nonnegative-integer? = 800 height : nonnegative-integer? = 200
procedure
(vector-pair-draw/magnitude left right #:title title [ #:width width #:height height]) → void? left : (vectorof complex?) right : (vectorof complex?) title : string? width : nonnegative-integer? = 800 height : nonnegative-integer? = 200
procedure
(vector-draw/real/imag vec #:title title [ #:width width #:height height]) → void? vec : (vectorof complex?) title : string? width : nonnegative-integer? = 800 height : nonnegative-integer? = 200
7 RSound Utilities
procedure
(make-harm3tone frequency volume? frames sample-rate) → rsound? frequency : nonnegative-number? volume? : nonnegative-number? frames : nonnegative-integer? sample-rate : nonnegative-number?
procedure
pitch : nonnegative-number? volume : nonnegative-number? duration : nonnegative-exact-integer?
procedure
(rsound-fft/left rsound) → (vectorof complex?)
rsound : rsound?
procedure
(rsound-fft/right rsound) → (vectorof complex?)
rsound : rsound?
procedure
(midi-note-num->pitch note-num) → number?
note-num : nonnegative-integer?
procedure
(fir-filter delay-lines) → procedure?
delay-lines : (listof (list/c nonnegative-exact-integer? real-number?))
So, for instance,
...would produce a filter that added the current frame to 4/10 of the input frame 13 frames ago and 1/10 of the input frame 4 frames ago.
procedure
(iir-filter delay-lines) → procedure?
delay-lines : (listof (list/c nonnegative-exact-integer? real-number?))
So, for instance,
...would produce a filter that added the current frame to 4/10 of the output frame 13 frames ago and 1/10 of the output frame 4 frames ago.
8 Frequency Response
procedure
(response-plot poly dbrel min-freq max-freq) → void?
poly : procedure? dbrel : real? min-freq : real? max-freq : real
procedure
(poles&zeros->fun poles zeros) → procedure?
poles : (listof real?) zeros : (listof real?)
(response-plot (poles&zeros->fun '(0.5 0.5+0.5i 0.5-0.5i) '(0+1i 0-1i)) 40 0 22050)
9 Filtering
(define (control f) (+ 0.5 (* 0.2 (sin (* f 7.123792865282977e-05))))) (define (sawtooth f) (/ (modulo f 220) 220)) (play (signal->rsound 88200 (lpf/dynamic control sawtooth)))
10 Single-cycle sounds
procedure
(synth-note family spec midi-note-number duration) → rsound family : string? spec : number-or-path? midi-note-number : natural? duration : natural?
(synth-note "vgame" 49 60 22010)
procedure
(synth-note/raw family spec midi-note-number duration) → rsound family : string? spec : number-or-path? midi-note-number : natural? duration : natural?
11 Stream-based Playing
procedure
(current-time/s) → natural?
12 Sample Code
An example of a signal that plays two lines, each with randomly changing square-wave tones. This one runs in the Intermediate student language:
(require (planet clements/rsound)) (require (planet clements/rsound/filter)) ; scrobble: number number number -> signal ; return a signal that generates square-wave tones, changing ; at the given interval into a new randomly-chosen frequency ; between lo-f and hi-f (define (scrobble change-interval lo-f hi-f) (local [(define freq-range (floor (- hi-f lo-f))) (define (maybe-change f l) (cond [(= l 0) (+ lo-f (random freq-range))] [else f]))] (network () [looper ((loop-ctr change-interval 1))] [freq (maybe-change (prev freq 400) looper)] [a (square-wave freq)]))) (define my-signal (network () [a ((scrobble 4000 200 600))] [b ((scrobble 40000 100 200))] [lpf-wave (sine-wave 0.1)] [c (lpf/dynamic (max 0.01 (abs (* 0.5 lpf-wave))) (+ a b))] [b (* c 0.1)])) ; write 20 seconds to a file, if uncommented: ; (rs-write (signal->rsound (* 20 44100) my-signal) "/tmp/foo.wav") ; play the signal (signal-play my-signal)
13 Reporting Bugs
For Heaven’s sake, report lots of bugs!