Portaudio: Bindings for the Portaudio portable sound library
1 Playing Sounds
The first high-level interface involves copying the entire sound into a malloc’ed buffer, and then playing it. This is relatively low-latency. On the other hand, copying the sound involves doubling the memory required for the sound itself, so it’s a bad idea to call this for sounds that are really big (> 100MB?).
(s16vec-play s16vec start-frame end-frame sample-rate) → (-> void?) s16vec : s16vector? start-frame : nat? end-frame : nat? sample-rate : nonnegative-real?
This function signals an error if start and end frames are not ordered and legal.
Here’s an example of a short program that plays a sine wave at 426 Hz for 2 seconds:
#lang racket (require (planet clements/portaudio) ffi/vector) (define pitch 426) (define sample-rate 44100.0) (define tpisr (* 2 pi (/ 1.0 sample-rate))) (define (real->s16 x) (inexact->exact (round (* 32767 x)))) (define vec (make-s16vector (* 88200 2))) (for ([t (in-range 88200)]) (define sample (real->s16 (* 0.2 (sin (* tpisr t pitch))))) (s16vector-set! vec (* 2 t) sample) (s16vector-set! vec (add1 (* 2 t)) sample)) (s16vec-play vec 0 88200 sample-rate)
2 Playing Streams
(stream-play buffer-filler buffer-frames sample-rate) → (list/c (-> real?) (-> void?)) buffer-filler : (-> buffer-setter? nat? nat? void?) buffer-frames : nat? sample-rate : nonnegative-real?
The function returns a list contaning two functions: one that queries the stream for a time in seconds, and the other that stops the stream.
This function is believed safe; it should not be possible to crash DrRacket by using this function badly (unless you exhaust memory by choosing an enormous buffer size).
Here’s an example of a program that uses stream-play to play a constant pitch of 426 Hz forever:
#lang racket (require (planet clements/portaudio)) (define (buffer-filler setter frames idx) (define base-t (* frames idx)) (for ([i (in-range frames)] [t (in-range base-t (+ base-t frames))]) (define pitch 426) (define sample (real->s16 (* 0.2 (sin (* tpisr t pitch))))) (setter (* i 2) sample) (setter (+ 1 (* i 2)) sample))) (define sample-rate 44100.0) (define tpisr (* 2 pi (/ 1.0 sample-rate))) (define (real->s16 x) (inexact->exact (round (* 32767 x)))) (match-define (list timer stopper) (stream-play buffer-filler 8192 44100.0))
Note that this example uses a large buffer size of 8K, so that most GC pauses won’t interrupt it (8192 / 44100.0 = 186 ms, a pretty long GC).
However, this will mean a latency of 2x186ms = 374ms, which would be pretty terrible for an interactive system. I usually use 1024 frames, and just put up with the occasional miss in return for lower latency.
(stream-play/unsafe buffer-filler buffer-frames sample-rate) → (list/c (-> real?) (-> void?)) buffer-filler : (-> cpointer? int? int? void?) buffer-frames : nat? sample-rate : nonnegative-real?
The difference is that this function’s callback is called with a cpointer, rather than a set!-proxy. This saves the overhead of a function call and several checks, but perhaps more importantly allows the use of functions like memcpy and vector-add that can operate at much higher speeds (currently ~5x) than the current vector operations.