Creating simple AIFF audio files
DrScheme seems to have little support for sound.
Presented here, sample code for writing 16 bit monophonic AIFF audio files, and a few procedures for sound synthesis in order to test it. AIFF files are lossless Interchange File Format-derived audio files used by Apple and SGI, and should hopefully be playable on any modern computer. The reader is advised to consult the internet for the intimate details of AIFF specifically, and the general concepts of IFF. In short, they are a binary format containing nested "chunks" of data. Each "chunk" consists of a 4 character ID tag, followed by 4 bytes representing the chunk size, then the chunk's contents.
A minimal AIFF file contains one top-level chunk, with the ID of "FORM". This contains inside it a 4 byte type of "AIFF" followed by two chunks, "COMM" and "SSND". COMM, the Common chunk, holds information on the bit size, sample frame rate, and so forth. SSND contains a bitstream representing the audio.
Upon running this recipe, it will generate 20 seconds of complex audio and write it to a file called "test.aiff", which will be monaural, 16 bit, and at 22kHz sample rate.
enchunk is a general-purpose function for encoding IFF chunks.
buffer->aiff converts a bytestring of sample frames into a bytestring representing an entire AIFF file.
sample-to-bytes converts a continuous value between -1 and 1 into a 16 bit sample.
op->buffer converts a function of time representing a waveform ("operator") into a buffer (bytestring) representing a waveform.
Other procedures and constants are essentially just defined for amusement value!
%begin scheme%
(define rate44kHz (cons 44100 (bytes #xAC #x44 #x00 #x00)))
(define rate22kHz (cons 22255 (bytes #x56 #xEE #x8B #xA3)))
(define A440 440) (define A480 480)
(define (linear-pitch-space p &opt (middleA A440))
(define (log2 n) (/ (log n) (log 2)))
(expt 2
(+ (/ (- p 69)
12)
(log2 middleA))))
(define (sample-to-bytes x)
(integer->integer-bytes (inexact->exact (floor (* x 32000))) 2 #t #t))
(define (enchunk ID buffer &opt (prefix (bytes)))
(let* ((l (+ (bytes-length buffer)
(bytes-length prefix)))
(nobytes (bytes))
(padded (if (odd? l)
(+ 1 l)
l)))
(if (eq? (string-length ID) 4)
(bytes-append (string->bytes/locale ID)
(integer->integer-bytes padded 4 #f #t)
(if prefix prefix nobytes)
buffer
(if (odd? l) (bytes 0) nobytes))
(error "ID must be 4 characters long!"))))
(define (op->buffer operator t &opt (rate rate44kHz))
(let ((buffer (make-bytes (* 2
(+ 1 (* t (car rate))))))
(end t)
(inc (/ 1 (car rate))))
(let loop ((x 0)
(byteoffset 0))
(if (> x end)
buffer
(begin (bytes-copy! buffer
byteoffset
(sample-to-bytes (operator x)))
(loop (+ x inc) (+ byteoffset 2)))))))
(define (buffer->aiff bitstream &opt (rate rate44kHz))
(let ((frames (/ (bytes-length bitstream) 2)))
(enchunk "FORM"
(bytes-append
(enchunk "COMM"
(bytes-append
(integer->integer-bytes 1 2 #t #t) (integer->integer-bytes frames 4 #f #t) (integer->integer-bytes 16 2 #t #t) (bytes #x40 #x0E) (cdr rate) (bytes 0 0 0 0))) (enchunk "SSND"
bitstream
(make-bytes 8 0))) (string->bytes/locale "AIFF"))))
(define (sinewave freq)
(lambda (x) (sin (* x freq 6.2832))))
(define (noise)
(lambda (x) (random)))
(define (ampmod signal mod)
(lambda (x) (* (signal x)
(mod x))))
(define (half-wave op)
(lambda (x)
(let ((b (op x)))
(if (> b 0) b 0))))
(define (FMoperator freq modulation)
(lambda (x)
(sin (+ (* x freq 6.2832)
(modulation x)))))
(define (mix &rest sources)
(lambda (x)
(/ (foldl (lambda (operation sum)
(+ sum
(operation x)))
0
sources)
(length sources))))
(define (amp a op)
(lambda (x)
(let ((b (* a (op x))))
(cond ((< b -1) -1)
((> b 1) 1)
(else b)))))
(define complex-sound (op->buffer (mix (ampmod (mix (sinewave (linear-pitch-space 60))
(sinewave (linear-pitch-space 64))
(sinewave (linear-pitch-space 67))) (half-wave (amp 10 (FMoperator 8
(sinewave 1))))) (amp 0.3 (ampmod (noise)
(sinewave 0.08))) (ampmod (amp 10 (FMoperator (linear-pitch-space 58)
(sinewave (/ (linear-pitch-space 58) 3)))) (sinewave 0.01))) 20
rate22kHz))
(with-output-to-file "test.aiff"
(thunk (display (buffer->aiff complex-sound rate22kHz)))
'replace)
%end%
Converting this to write WAV files should be trivial. There seems to be a few omissions in the version of the AIFF spec I was using, hence a few (clearly marked) kludges have been used to get it working. Two sample rate constants have been defined, one for 22kHz and one for 44kHz. Currently, bit depth is hard-wired and only one audio channel is supported (ie. mono). When using
linear-pitch-space, a value of 60 represents middle C, 61 represents C sharp, 59 represents B and so forth.
Extensive use is made of extensions provided by Swindle.
AIFF writing is dog-slow, and it doesn't sound like a Moog.
To start experimenting more simply, replace the complex sound mix with
(sinewave A440) or
(noise) for middle A and white noise respectively.
--
RitchieSmith - 15 Aug 2006