summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile.am1
-rw-r--r--chickadee/audio.scm763
-rw-r--r--doc/api.texi458
3 files changed, 1220 insertions, 2 deletions
diff --git a/Makefile.am b/Makefile.am
index 6183b03..23d7e5a 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -59,6 +59,7 @@ SOURCES = \
chickadee/audio/openal.scm \
chickadee/audio/vorbis.scm \
chickadee/audio/wav.scm \
+ chickadee/audio.scm \
chickadee/render/color.scm \
chickadee/render/gl.scm \
chickadee/render/gpu.scm \
diff --git a/chickadee/audio.scm b/chickadee/audio.scm
new file mode 100644
index 0000000..aa56dac
--- /dev/null
+++ b/chickadee/audio.scm
@@ -0,0 +1,763 @@
+;;; Chickadee Game Toolkit
+;;; Copyright © 2020, 2019 David Thompson <davet@gnu.org>
+;;;
+;;; Chickadee is free software: you can redistribute it and/or modify
+;;; it under the terms of the GNU General Public License as published
+;;; by the Free Software Foundation, either version 3 of the License,
+;;; or (at your option) any later version.
+;;;
+;;; Chickadee is distributed in the hope that it will be useful, but
+;;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+;;; General Public License for more details.
+;;;
+;;; You should have received a copy of the GNU General Public License
+;;; along with this program. If not, see
+;;; <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; Audio API.
+;;
+;;; Code:
+
+(define-module (chickadee audio)
+ #:use-module (chickadee array-list)
+ #:use-module (chickadee audio mpg123)
+ #:use-module ((chickadee audio openal) #:prefix openal:)
+ #:use-module (chickadee audio vorbis)
+ #:use-module (chickadee audio wav)
+ #:use-module (chickadee math)
+ #:use-module (chickadee math vector)
+ #:use-module (ice-9 format)
+ #:use-module (ice-9 match)
+ #:use-module (rnrs bytevectors)
+ #:use-module (srfi srfi-1)
+ #:use-module (srfi srfi-9)
+ #:use-module (srfi srfi-9 gnu)
+ #:export (init-audio
+ quit-audio
+ update-audio
+ load-audio
+ audio?
+ streaming-audio?
+ static-audio?
+ audio-mode
+ audio-duration
+ audio-bits-per-sample
+ audio-channels
+ audio-sample-rate
+ audio-play
+ make-source
+ source?
+ streaming-source?
+ static-source?
+ source-playing?
+ source-paused?
+ source-stopped?
+ source-pitch
+ source-volume
+ source-min-volume
+ source-max-volume
+ source-max-distance
+ source-reference-distance
+ source-rolloff-factor
+ source-cone-outer-volume
+ source-cone-inner-angle
+ source-cone-outer-angle
+ source-position
+ source-velocity
+ source-direction
+ source-relative?
+ source-play
+ source-pause
+ source-toggle
+ source-stop
+ source-rewind
+ set-source-audio!
+ set-source-loop!
+ set-source-pitch!
+ set-source-volume!
+ set-source-min-volume!
+ set-source-max-volume!
+ set-source-max-distance!
+ set-source-reference-distance!
+ set-source-rolloff-factor!
+ set-source-cone-outer-volume!
+ set-source-cone-inner-angle!
+ set-source-cone-outer-angle!
+ set-source-position!
+ set-source-velocity!
+ set-source-direction!
+ set-source-relative!)
+ #:re-export ((openal:listener-volume . listener-volume)
+ (openal:listener-position . listener-position)
+ (openal:listener-velocity . listener-velocity)
+ (openal:listener-orientation . listener-orientation)
+ (openal:set-listener-volume! . set-listener-volume!)
+ (openal:set-listener-position! . set-listener-position!)
+ (openal:set-listener-velocity! . set-listener-velocity!)
+ (openal:set-listener-orientation! . set-listener-orientation!)
+ (openal:doppler-factor . doppler-factor)
+ (openal:speed-of-sound . speed-of-sound)
+ (openal:distance-model . distance-model)
+ (openal:set-doppler-factor! . set-doppler-factor!)
+ (openal:set-speed-of-sound! . set-speed-of-sound!)
+ (openal:set-distance-model! . set-distance-model!)))
+
+(define %default-cone-outer-angle (* 2.0 pi))
+(define %default-max-distance (expt 2 31))
+
+(define-record-type <audio>
+ (%make-audio mode bv duration bits-per-sample channels sample-rate
+ decode-proc seek-proc close-proc)
+ audio?
+ (mode audio-mode)
+ (bv audio-bv)
+ (static-length audio-static-length set-audio-static-length!)
+ (duration audio-duration)
+ (bits-per-sample audio-bits-per-sample)
+ (channels audio-channels)
+ (sample-rate audio-sample-rate)
+ (decode-proc audio-decode-proc)
+ (seek-proc audio-seek-proc)
+ (close-proc audio-close-proc))
+
+(define-record-type <source>
+ (%make-source audio openal-source)
+ source?
+ (audio source-audio %set-source-audio!)
+ (openal-source source-openal-source)
+ (loop? source-loop? %set-source-loop!)
+ (pitch source-pitch %set-source-pitch!)
+ (volume source-volume %set-source-volume!)
+ (min-volume source-min-volume %set-source-min-volume!)
+ (max-volume source-max-volume %set-source-max-volume!)
+ (max-distance source-max-distance %set-source-max-distance!)
+ (reference-distance source-reference-distance %set-source-reference-distance!)
+ (rolloff-factor source-rolloff-factor %set-source-rolloff-factor!)
+ (cone-outer-volume source-cone-outer-volume %set-source-cone-outer-volume!)
+ (cone-inner-angle source-cone-inner-angle %set-source-cone-inner-angle!)
+ (cone-outer-angle source-cone-outer-angle %set-source-cone-outer-angle!)
+ (position source-position %set-source-position!)
+ (velocity source-velocity %set-source-velocity!)
+ (direction source-direction %set-source-direction!)
+ (relative? source-relative? %set-source-relative!))
+
+(define-record-type <sound-system> ; gonna bring me back home
+ (%make-sound-system openal-context free-buffers used-buffers
+ sources streaming-sources free-sources used-sources
+ guardian)
+ sound-system?
+ (openal-context sound-system-openal-context)
+ (free-buffers sound-system-free-buffers)
+ (used-buffers sound-system-used-buffers)
+ (sources sound-system-sources)
+ (streaming-sources sound-system-streaming-sources)
+ ;; free/used temp sources for audio-play procedure.
+ (free-sources sound-system-free-sources)
+ (used-sources sound-system-used-sources)
+ (guardian sound-system-guardian))
+
+(define (make-sound-system)
+ (%make-sound-system (openal:make-context (openal:open-device))
+ (make-array-list)
+ (make-hash-table)
+ (make-weak-key-hash-table)
+ (make-hash-table)
+ (make-array-list)
+ (make-hash-table)
+ (make-guardian)))
+
+(define (register-source sound-system source)
+ (hashq-set! (sound-system-sources sound-system) source source)
+ ((sound-system-guardian sound-system) source))
+
+(define (borrow-buffer sound-system)
+ (let* ((free (sound-system-free-buffers sound-system))
+ (used (sound-system-used-buffers sound-system))
+ (buffer (if (array-list-empty? free)
+ (openal:make-buffer)
+ (array-list-pop! free))))
+ (hashv-set! used (openal:buffer-id buffer) buffer)))
+
+(define (return-buffer sound-system buffer-id)
+ (let* ((free (sound-system-free-buffers sound-system))
+ (used (sound-system-used-buffers sound-system))
+ (buffer (hashv-ref used buffer-id)))
+ (hashv-remove! used buffer-id)
+ (array-list-push! free buffer)))
+
+(define (borrow-source sound-system)
+ (let* ((free (sound-system-free-sources sound-system))
+ (used (sound-system-used-sources sound-system))
+ (source (if (array-list-empty? free)
+ (make-source)
+ (array-list-pop! free))))
+ (hashq-set! used source source)))
+
+(define (return-source sound-system source)
+ (let ((free (sound-system-free-sources sound-system))
+ (used (sound-system-used-sources sound-system)))
+ (when (hashq-remove! used source)
+ (array-list-push! free source))))
+
+(define (start-sound-system sound-system)
+ (openal:set-current-context! (sound-system-openal-context sound-system))
+ (mpg123-init))
+
+(define (stop-sound-system sound-system)
+ (let ((context (sound-system-openal-context sound-system)))
+ ;; Delete sources.
+ (hash-for-each (lambda (key source)
+ (openal:delete-source (source-openal-source source)))
+ (sound-system-sources sound-system))
+ ;; Delete buffers.
+ (array-list-for-each (lambda (i buffer)
+ (openal:delete-buffer buffer))
+ (sound-system-free-buffers sound-system))
+ (hash-for-each (lambda (id buffer)
+ (openal:delete-buffer buffer))
+ (sound-system-used-buffers sound-system))
+ ;; Delete context.
+ (openal:set-current-context! #f)
+ (openal:destroy-context context)
+ (openal:close-device (openal:context-device context))))
+
+(define (update-sound-system sound-system)
+ ;; TODO: Audio should really be handled on a dedicated thread, but
+ ;; that's a task for another day/year.
+ ;;
+ ;; Feed chunks of audio data to streaming sources that need more
+ ;; data.
+ (let ((streaming-sources (sound-system-streaming-sources sound-system)))
+ (hash-for-each (lambda (key source)
+ (let loop ()
+ ;; Unqueue a buffer that has been played, if
+ ;; there is one.
+ (when (source-flush-buffer source)
+ ;; Replace the unqueued buffer with the next
+ ;; chunk of audio. If there is nothing left
+ ;; to decode, remove the source from the list
+ ;; of actively streaming sources.
+ (if (source-buffer/stream source)
+ (loop)
+ (hashq-remove! streaming-sources source)))))
+ streaming-sources))
+ ;; Check if any borrowed sources (used for audio-play procedure) are
+ ;; stopped and return them to the queue for reuse.
+ (hash-for-each (lambda (key source)
+ (when (source-stopped? source)
+ (return-source sound-system source)))
+ (sound-system-used-sources sound-system))
+ ;; Delete OpenAL sources associated with sounds that are ready to be
+ ;; GC'd.
+ (let ((guardian (sound-system-guardian sound-system)))
+ (let loop ((source (guardian)))
+ (when source
+ (when (streaming-source? source)
+ ;; Clean up audio resources, like open file handles.
+ (audio-close (source-audio source))
+ ;; Ensuring that the source is stopped means that we can
+ ;; reliably reclaim all buffers still attached to the source
+ ;; before we delete it, otherwise we could leak memory
+ ;; because the buffers never make it back into the buffer
+ ;; pool.
+ (openal:source-stop (source-openal-source source))
+ (let loop ()
+ (and (source-flush-buffer source) (loop))))
+ (openal:delete-source (source-openal-source source))
+ (loop (guardian))))))
+
+(define (stop-all-sources sound-system)
+ (hash-for-each (lambda (key source)
+ (source-stop source))
+ (sound-system-sources sound-system)))
+
+(define (add-streaming-source sound-system source)
+ (hashq-set! (sound-system-streaming-sources sound-system) source source)
+ ;; Initialize the source's queue with a bunch of buffers, enough to
+ ;; keep OpenAL busy until the next sound system update.
+ ;; update-audio-system will add new buffers as they are unqueued
+ ;; later.
+ (let loop ((i 0))
+ (when (< i 8) ; TODO: allow variable number of buffers?
+ (source-buffer/stream source)
+ (loop (+ i 1)))))
+
+(define (remove-streaming-source sound-system source)
+ ;; Flush buffers.
+ (let loop ()
+ (and (source-flush-buffer source) (loop)))
+ ;; Remove from actively playing sources.
+ (hashq-remove! (sound-system-streaming-sources sound-system) source))
+
+(define current-sound-system (make-parameter #f))
+
+(define (init-audio)
+ "Initialize audio system."
+ (let ((sound-system (make-sound-system)))
+ (start-sound-system sound-system)
+ (current-sound-system sound-system)))
+
+(define (quit-audio)
+ "Stop audio system."
+ (let ((sound-system (current-sound-system)))
+ (when sound-system
+ (stop-sound-system sound-system)
+ (current-sound-system #f))))
+
+(define (update-audio)
+ "Update audio system."
+ (update-sound-system (current-sound-system)))
+
+(define* (make-source #:key
+ audio
+ loop?
+ (pitch 1.0)
+ (volume 1.0)
+ (min-volume 0.0)
+ (max-volume 1.0)
+ (max-distance %default-max-distance)
+ (reference-distance 0.0)
+ (rolloff-factor 1.0)
+ (cone-outer-volume 0.0)
+ (cone-inner-angle 0.0)
+ (cone-outer-angle %default-cone-outer-angle)
+ (position (vec3 0.0 0.0 0.0))
+ (velocity (vec3 0.0 0.0 0.0))
+ (direction (vec3 0.0 0.0 0.0))
+ relative?)
+ "Return a new audio source. AUDIO is the audio data to use when
+playing. LOOP? specifies whether or not to loop the audio during
+playback. PITCH is a scalar that modifies the pitch of the audio
+data. VOLUME is a scalar that modifies the volume of the audio data.
+
+For 3D audio representation, many other arguments are available.
+POSITION is a 3D vector. VELOCITY is a 3D vector. DIRECTION is a 3D
+vector. RELATIVE? specifies whether or not to consider the source
+position as relative to the listener or as an absolute value."
+ (let ((source (%make-source audio (openal:make-source))))
+ (register-source (current-sound-system) source)
+ (set-source-loop! source loop?)
+ (set-source-pitch! source pitch)
+ (set-source-volume! source volume)
+ (set-source-min-volume! source min-volume)
+ (set-source-max-volume! source max-volume)
+ (set-source-max-distance! source max-distance)
+ (set-source-reference-distance! source reference-distance)
+ (set-source-rolloff-factor! source rolloff-factor)
+ (set-source-cone-outer-volume! source cone-outer-volume)
+ (set-source-cone-inner-angle! source cone-inner-angle)
+ (set-source-cone-outer-angle! source cone-outer-angle)
+ (set-source-position! source position)
+ (set-source-velocity! source velocity)
+ (set-source-direction! source direction)
+ (set-source-relative! source relative?)
+ source))
+
+(define (streaming-source? source)
+ "Return #t if SOURCE is currently configured with streaming audio
+data."
+ (let ((audio (source-audio source)))
+ (and audio (streaming-audio? audio))))
+
+(define (static-source? source)
+ "Return #t if SOURCE is currently configured with static audio data"
+ (let ((audio (source-audio source)))
+ (and audio (static-audio? audio))))
+
+(define (set-source-audio! source audio)
+ "Set the playback stream for SOURCE to AUDIO."
+ (%set-source-audio! source audio)
+ (when (static-audio? audio)
+ (source-buffer/static source)))
+
+(define (source-play source)
+ "Begin/resume playback of SOURCE."
+ (unless (source-playing? source)
+ (openal:source-play (source-openal-source source))
+ (when (streaming-source? source)
+ (add-streaming-source (current-sound-system) source))))
+
+(define (source-pause source)
+ "Pause playback of SOURCE."
+ (openal:source-pause (source-openal-source source)))
+
+(define (source-toggle source)
+ "Play SOURCE if it is currently paused or pause SOURCE if it is
+currently playing."
+ (if (source-playing? source)
+ (source-pause source)
+ (source-play source)))
+
+(define* (source-stop #:optional source)
+ "Stop playing SOURCE or, if no source is specified, stop playing *all*
+sources."
+ (cond
+ ((not source)
+ (stop-all-sources (current-sound-system)))
+ ((streaming-source? source)
+ (openal:source-stop (source-openal-source source))
+ (audio-seek (source-audio source) 0)
+ (remove-streaming-source (current-sound-system) source))
+ (else
+ (openal:source-stop (source-openal-source source)))))
+
+(define (source-rewind source)
+ "Rewind SOURCE back to the beginning."
+ (if (streaming-source? source)
+ (audio-seek (source-audio source) 0)
+ (openal:source-rewind (source-openal-source source))))
+
+(define (source-playing? source)
+ "Return #t if SOURCE is currently playing."
+ (eq? (openal:source-state (source-openal-source source)) 'playing))
+
+(define (source-paused? source)
+ "Return #t if SOURCE is currently paused."
+ (eq? (openal:source-state (source-openal-source source)) 'paused))
+
+(define (source-stopped? source)
+ "Return #t if SOURCE is currently stopped."
+ (eq? (openal:source-state (source-openal-source source)) 'stopped))
+
+(define (set-source-loop! source loop?)
+ "Configure whether or not SOURCE should loop the audio stream."
+ (%set-source-pitch! source loop?)
+ (openal:set-source-looping! (source-openal-source source) loop?))
+
+(define (set-source-pitch! source pitch)
+ "Set the pitch multiplier for SOURCE to PITCH."
+ (unless (>= pitch 0.0)
+ (error "pitch must be a positive number" pitch))
+ (%set-source-pitch! source pitch)
+ (openal:set-source-property! (source-openal-source source) 'pitch pitch))
+
+(define (set-source-volume! source volume)
+ "Set the volume of SOURCE to VOLUME. A value of 1.0 is 100% volume."
+ (unless (>= volume 0.0)
+ (error "volume must be a positive number" volume))
+ (%set-source-volume! source volume)
+ (openal:set-source-property! (source-openal-source source) 'gain volume))
+
+(define (set-source-min-volume! source volume)
+ "Set the minimum volume of SOURCE to VOLUME."
+ (unless (>= volume 0.0)
+ (error "minimum volume must be a positive number" volume))
+ (%set-source-min-volume! source volume)
+ (openal:set-source-property! (source-openal-source source) 'min-gain volume))
+
+(define (set-source-max-volume! source volume)
+ "Set the maximum volume of SOURCE to VOLUME."
+ (unless (>= volume 0.0)
+ (error "maximum volume must be a positive number" volume))
+ (%set-source-max-volume! source volume)
+ (openal:set-source-property! (source-openal-source source) 'max-gain volume))
+
+(define (set-source-max-distance! source distance)
+ "Set the distance where there will no longer be any attenuation of
+SOURCE to DISTANCE."
+ (%set-source-max-distance! source distance)
+ (openal:set-source-property! (source-openal-source source) 'max-distance distance))
+
+(define (set-source-reference-distance! source distance)
+ "Set the reference distance of SOURCE to DISTANCE."
+ (%set-source-reference-distance! source distance)
+ (openal:set-source-property! (source-openal-source source) 'reference-distance distance))
+
+(define (set-source-rolloff-factor! source factor)
+ "Set the rolloff factor of SOURCE to FACTOR."
+ (%set-source-rolloff-factor! source factor)
+ (openal:set-source-property! (source-openal-source source) 'rolloff-factor factor))
+
+(define (set-source-cone-outer-volume! source volume)
+ "Set the volume of SOURCE when outside the sound cone to VOLUME."
+ (unless (>= volume 0.0)
+ (error "cone outer volume must be a positive number" volume))
+ (%set-source-cone-outer-volume! source volume)
+ (openal:set-source-property! (source-openal-source source) 'cone-outer-gain volume))
+
+(define (set-source-cone-inner-angle! source angle)
+ "Set the inner angle of the source cone of SOURCE to ANGLE radians."
+ (%set-source-cone-inner-angle! source angle)
+ (openal:set-source-property! (source-openal-source source)
+ 'cone-inner-angle
+ (radians->degrees angle)))
+
+(define (set-source-cone-outer-angle! source angle)
+ "Set the outer angle of the source cone of SOURCE to ANGLE radians."
+ (%set-source-cone-outer-angle! source angle)
+ (openal:set-source-property! (source-openal-source source)
+ 'cone-outer-angle
+ (radians->degrees angle)))
+
+(define (set-source-position! source position)
+ "Set the position of SOURCE to the 3D vector POSITION."
+ (%set-source-position! source position)
+ (openal:set-source-property! (source-openal-source source) 'position position))
+
+(define (set-source-velocity! source velocity)
+ "Set the velocity of SOURCE to the 3D vector VELOCITY."
+ (%set-source-velocity! source velocity)
+ (openal:set-source-property! (source-openal-source source) 'velocity velocity))
+
+(define (set-source-direction! source direction)
+ "Set the direction of SOURCE to the 3D vector DIRECTION."
+ (%set-source-direction! source direction)
+ (openal:set-source-property! (source-openal-source source) 'direction direction))
+
+(define (set-source-relative! source relative?)
+ "If RELATIVE? is #t, set the position of SOURCE to be relative to the
+listener. Otherwise, the position is in absolute coordinates."
+ (%set-source-relative! source relative?)
+ (openal:set-source-property! (source-openal-source source) 'relative relative?))
+
+(define (source-flush-buffer source)
+ (let* ((openal-source (source-openal-source source))
+ (processed (openal:source-buffers-processed openal-source)))
+ (if (> processed 0)
+ (let* ((buffer-id (openal:source-unqueue-buffer openal-source)))
+ (return-buffer (current-sound-system) buffer-id)
+ #t)
+ #f)))
+
+(define (source-buffer/static source)
+ (let* ((audio (source-audio source))
+ (buffer (borrow-buffer (current-sound-system)))
+ (bv (audio-bv audio))
+ (length (audio-static-length audio))
+ (format (audio-format audio))
+ (sample-rate (audio-sample-rate audio)))
+ (openal:set-buffer-data! buffer bv length format sample-rate)
+ (openal:set-source-buffer! (source-openal-source source) buffer)))
+
+(define (source-buffer/stream source)
+ (let* ((audio (source-audio source)))
+ (define (next)
+ (call-with-values (lambda () (audio-decode audio))
+ (lambda (chunk length)
+ (cond
+ ((> length 0)
+ (let ((format (audio-format audio))
+ (sample-rate (audio-sample-rate audio))
+ (buffer (borrow-buffer (current-sound-system))))
+ (openal:set-buffer-data! buffer chunk length format sample-rate)
+ (openal:source-queue-buffer (source-openal-source source) buffer)
+ (when (source-stopped? source)
+ (source-play source))
+ #t))
+ ((source-loop? source)
+ ;; Reset back to the beginning and try again.
+ (audio-seek audio 0)
+ (next))
+ ;; ight imma head out (if the git history is lost,
+ ;; researchers will be able to accurately date the code
+ ;; based upon this meme)
+ (else #f)))))
+ (next)))
+
+(define* (make-audio #:key
+ mode
+ (buffer-size 4096)
+ bits-per-sample
+ channels
+ duration
+ sample-rate
+ decode
+ seek
+ close)
+ (let* ((bv-size (if (eq? mode 'stream)
+ buffer-size
+ ;; Make a buffer big enough to hold the entire
+ ;; decoded audio stream.
+ (inexact->exact
+ (ceiling
+ (* duration
+ (/ bits-per-sample 8)
+ channels
+ sample-rate
+ ;; XXX: Adding extra padding because vorbis files
+ ;; seem to lie about how many audio samples there
+ ;; are.
+ 1.1)))))
+ (audio (%make-audio mode
+ (make-bytevector bv-size)
+ duration
+ bits-per-sample
+ channels
+ sample-rate
+ decode
+ seek
+ close)))
+ (when (eq? mode 'static)
+ (call-with-values (lambda () (audio-decode audio))
+ (lambda (bv length)
+ (set-audio-static-length! audio length)))
+ (audio-close audio))
+ audio))
+
+(define (display-audio audio port)
+ (format port "#<audio mode: ~a duration: ~f bits-per-sample: ~d sample-rate: ~d>"
+ (audio-mode audio)
+ (audio-duration audio)
+ (audio-bits-per-sample audio)
+ (audio-sample-rate audio)))
+
+(set-record-type-printer! <audio> display-audio)
+
+(define (streaming-audio? audio)
+ "Return #t if AUDIO is in streaming mode."
+ (eq? (audio-mode audio) 'stream))
+
+(define (static-audio? audio)
+ "Return #t if AUDIO is in static mode."
+ (eq? (audio-mode audio) 'static))
+
+(define (audio-format audio)
+ (let ((channels (audio-channels audio))
+ (bits-per-sample (audio-bits-per-sample audio)))
+ (cond
+ ((and (= channels 1)
+ (= bits-per-sample 16))
+ 'mono-16)
+ ((and (= channels 1)
+ (= bits-per-sample 8))
+ 'mono-8)
+ ((and (= channels 2)
+ (= bits-per-sample 16))
+ 'stereo-16)
+ ((and (= channels 2)
+ (= bits-per-sample 8))
+ 'stereo-8)
+ (else
+ (error "unsupported audio format" channels bits-per-sample)))))
+
+(define (audio-decode audio)
+ (define (nearest-multiple-of-4 x)
+ (let ((remainder (modulo x 4)))
+ (if (zero? remainder)
+ x
+ (+ x (- 4 remainder)))))
+ (let* ((bv (audio-bv audio))
+ (length ((audio-decode-proc audio) bv)))
+ (values bv (nearest-multiple-of-4 length))))
+
+(define (audio-close audio)
+ ((audio-close-proc audio)))
+
+(define (audio-seek audio t)
+ ((audio-seek-proc audio) t))
+
+(define* (audio-play audio #:key
+ (pitch 1.0)
+ (volume 1.0)
+ (min-volume 0.0)
+ (max-volume 1.0)
+ (max-distance %default-max-distance)
+ (reference-distance 0.0)
+ (rolloff-factor 1.0)
+ (cone-outer-volume 0.0)
+ (cone-inner-angle 0.0)
+ (cone-outer-angle %default-cone-outer-angle)
+ (position (vec3 0.0 0.0 0.0))
+ (velocity (vec3 0.0 0.0 0.0))
+ (direction (vec3 0.0 0.0 0.0))
+ relative?)
+ (let ((source (borrow-source (current-sound-system))))
+ (set-source-audio! source audio)
+ (set-source-pitch! source pitch)
+ (set-source-volume! source volume)
+ (set-source-min-volume! source min-volume)
+ (set-source-max-volume! source max-volume)
+ (set-source-max-distance! source max-distance)
+ (set-source-reference-distance! source reference-distance)
+ (set-source-rolloff-factor! source rolloff-factor)
+ (set-source-cone-outer-volume! source cone-outer-volume)
+ (set-source-cone-inner-angle! source cone-inner-angle)
+ (set-source-cone-outer-angle! source cone-outer-angle)
+ (set-source-position! source position)
+ (set-source-velocity! source velocity)
+ (set-source-direction! source direction)
+ (set-source-relative! source relative?)
+ (source-play source)))
+
+(define (make-wav-audio file-name mode)
+ (let ((wav (open-wav file-name)))
+ (make-audio #:mode mode
+ #:duration (/ (exact->inexact (wav-length wav))
+ (wav-sample-rate wav))
+ #:bits-per-sample (wav-bits-per-sample wav)
+ #:sample-rate (wav-sample-rate wav)
+ #:channels (wav-num-channels wav)
+ #:decode
+ (lambda (bv)
+ (wav-read wav bv))
+ #:seek
+ (lambda (t)
+ (wav-time-seek wav t))
+ #:close
+ (lambda ()
+ (close-wav wav)))))
+
+(define (make-vorbis-audio file-name mode)
+ (let* ((vf (vorbis-open file-name))
+ (info (vorbis-info vf)))
+ (make-audio #:mode mode
+ #:duration (vorbis-time-total vf)
+ #:bits-per-sample 16
+ #:sample-rate (vorbis-info-sample-rate info)
+ #:channels (vorbis-info-channels info)
+ #:decode
+ (lambda (bv)
+ (vorbis-fill-buffer vf bv))
+ #:seek
+ (lambda (t)
+ (vorbis-time-seek vf t))
+ #:close
+ (lambda ()
+ (vorbis-clear vf)))))
+
+(define (make-mp3-audio file-name mode)
+ (let ((handle (mpg123-open file-name)))
+ (call-with-values (lambda () (mpg123-format handle))
+ (lambda (sample-rate channels bits-per-sample)
+ (make-audio #:mode mode
+ #:bits-per-sample bits-per-sample
+ #:channels channels
+ #:duration (exact->inexact
+ (/ (mpg123-length handle) sample-rate))
+ #:sample-rate sample-rate
+ #:decode
+ (lambda (bv)
+ (mpg123-read handle bv))
+ #:seek
+ (lambda (t)
+ (mpg123-time-seek handle t))
+ #:close
+ (lambda ()
+ (mpg123-close handle)))))))
+
+(define* (load-audio file-name #:key (mode 'static))
+ "Load audio source in FILE-NAME. The following audio formats are
+supported:
+
+- WAV
+- Ogg Vorbis
+- MP3
+
+MODE determines the algorithm used for reading audio from the file.
+If MODE is 'static' then the entire file will be read into memory
+immediately. If MODE is 'stream', audio data will be need in small
+pieces as needed from the file. The default mode is 'static'. For
+short sound effects, 'static' is recommended. For music and other
+lengthy audio files, 'stream' is recommended."
+ (unless (file-exists? file-name)
+ (error "file not found" file-name))
+ (let* ((make (match (last (string-split file-name #\.))
+ ("wav" make-wav-audio)
+ ("ogg" make-vorbis-audio)
+ ("mp3" make-mp3-audio)
+ (ext
+ (error "unsupported audio file format:" ext)))))
+ (make file-name mode)))
diff --git a/doc/api.texi b/doc/api.texi
index f1726d9..18e0841 100644
--- a/doc/api.texi
+++ b/doc/api.texi
@@ -2787,8 +2787,462 @@ Return the data type of @var{attribute}.
@node Audio
@section Audio
-Most games need to play audio. Background music to set the mood and
-many sound effects for when things happen.
+A game isn't complete without sound. Most games play some cool
+background music to set the mood and have many sound effects to play
+when events happen. The @code{(chickadee audio)} module provides a
+robust audio API backed by the OpenAL 3D audio system.
+
+@menu
+* Audio Files:: Not audiophiles.
+* Sources:: Audio emitters.
+* The Listener:: The player's ears.
+* The Environment:: Global sound model settings.
+@end menu
+
+The basics of playing audio are very simple. Just load an audio file
+in the load hook (or anywhere else once the game loop is running) and
+play it!
+
+@example
+(use-modules (chickadee audio))
+
+(define audio #f)
+
+(define (load)
+ (set! audio (load-audio "neat-sound-effect.wav"))
+ (audio-play audio))
+
+(run-game #:load load)
+@end example
+
+For more advanced usage, check out the full API reference in the
+following sections.
+
+@node Audio Files
+@subsection Audio Files
+
+Sound data is represented by a special @code{<audio>} data type that
+stores not only the audio samples themselves, but metadata such as
+sample rate, number of channels, and how many bits are used for each
+sample.
+
+@deffn {Procedure} load-audio file-name [#:mode @code{static}]
+Load audio within @var{file-name}. The following file formats are
+currently supported:
+
+@itemize
+@item WAV
+@item MP3
+@item Ogg Vorbis
+@end itemize
+
+Audio files can be loaded in two different ways, as indicated by
+@var{mode}:
+
+@itemize
+@item static:
+Load the entire audio file into memory.
+@item stream:
+Load chunks of the audio file as needed.
+@end itemize
+
+Generally speaking, sound effects don't take up much space and should
+be loaded statically, but music files are much larger and should use
+streaming. Static loading is the default.
+@end deffn
+
+@deffn {Procedure} audio? @var{obj}
+Return @code{#t} if @var{obj} is an audio object.
+@end deffn
+
+@deffn {Procedure} streaming-audio? @var{audio}
+Return @code{#t} if @var{audio} uses stream loading.
+@end deffn
+
+@deffn {Procedure} static-audio?
+Return @code{#t} if @var{audio} uses static loading.
+@end deffn
+
+@deffn {Procedure} audio-mode audio
+Return the loading mode for @var{audio}, either @code{static} or
+@code{stream}.
+@end deffn
+
+@deffn {Procedure} audio-duration audio
+Return the duration of @var{audio} in seconds.
+@end deffn
+
+@deffn {Procedure} audio-bits-per-sample audio
+Return the number of bits per sample in @var{audio}.
+@end deffn
+
+@deffn {Procedure} audio-channels audio
+Return the number of channels in @var{audio}.
+@end deffn
+
+@deffn {Procedure} audio-sample-rate audio
+Return the sample rate of @var{audio}.
+@end deffn
+
+@deffn {Procedure} audio-play audio [#:pitch 1.0] [#:volume 1.0] @
+ [#:min-volume 0.0] [#:max-volume 1.0] [#:max-distance] @
+ [#:reference-distance 0.0] [#:rolloff-factor 1.0] @
+ [#:cone-outer-volume 0.0] [#:cone-inner-angle 0.0] @
+ [#:cone-outer-angle] @
+ [#:position @code{(vec3 0.0 0.0 0.0)}] @
+ [#:velocity @code{(vec3 0.0 0.0 0.0)}] @
+ [#:direction @code{(vec3 0.0 0.0 0.0)}] @
+ [#:relative? @code{#f}]
+
+Play @var{audio}. There are many, many knobs to tweak that will
+affect the sound that comes out of the player's speakers.:
+
+@itemize
+@item @var{pitch}:
+Pitch multiplier. The default value of 1.0 means no change in pitch.
+@item @var{volume}:
+Volume multiplier. The default value of 1.0 means no change in volume.
+@item @var{min-volume}:
+Minimum volume.
+@item @var{max-volume}:
+Maximum volume.
+@item @var{max-distance}:
+Used with the inverse clamped distance model (the default model) to
+set the distance where there will no longer be any attenuation of the
+source.
+@item @var{reference-distance}:
+The distance where the volume for the audio would drop by half (before
+being influenced by the rolloff factor or maximum distance.)
+@item @var{rolloff-factor}:
+For slowing down or speeding up the rate of attenuation. The default
+of 1.0 means no attenuation adjustment is made.
+@item @var{cone-outer-volume}:
+The volume when outside the oriented cone.
+@item @var{cone-inner-angle}:
+Inner angle of the sound cone, in radians. The default value is 0.
+@item @var{cone-outer-angle}:
+Outer angle of the sound cone, in radians. The default value is 2pi
+radians, or 360 degrees.
+@item @var{position}:
+The source of the sound emitter in 3D space.
+@item @var{velocity}:
+The velocity of the sound emitter in 3D space.
+@item @var{direction}:
+The direction of the sound emitter in 3D space.
+@item @var{relative?}:
+A flag that determines whether the position is in absolute coordinates
+or relative to the listener's location. Absolute coordinates are used
+by default.
+@end itemize
+
+For games with basic sound needs (that is to say they don't need 3D
+sound modeling), the only things that really matter are @var{volume}
+and @var{pitch}.
+
+@end deffn
+
+@node Sources
+@subsection Sources
+
+While the @code{audio-play} procedure provides a quick and convenient
+way to play audio, it has some limitations. What if the audio is a
+long piece of music that might need to be paused or stopped later?
+What if the audio should be looped? What if the volume or pitch needs
+to be altered? For manipulating audio in these ways, a ``source'' is
+required. Sources can be thought of like a boombox: They sit
+somewhere in the room and play sound. The pause or stop buttons can
+be pressed; it can be moved somewhere else; the volume knob can be
+adjusted; the CD can be changed.
+
+Sources are a great fit for handling background music, among other
+things. For quick, one-off sound effects, @code{audio-play} is a
+better fit.
+
+@deffn {Procedure} make-source audio loop? [#:pitch 1.0] [#:volume 1.0] @
+ [#:min-volume 0.0] [#:max-volume 1.0] [#:max-distance] @
+ [#:reference-distance 0.0] [#:rolloff-factor 1.0] @
+ [#:cone-outer-volume 0.0] [#:cone-inner-angle 0.0] @
+ [#:cone-outer-angle] @
+ [#:position @code{(vec3 0.0 0.0 0.0)}] @
+ [#:velocity @code{(vec3 0.0 0.0 0.0)}] @
+ [#:direction @code{(vec3 0.0 0.0 0.0)}] @
+ [#:relative? @code{#f}]
+
+Return a new audio source. @var{audio} is the audio data to use when
+playing. @var{loop?} specifies whether or not to loop the audio
+during playback.
+
+Refer to @code{audio-play} (@pxref{Audio Files}) for information about
+the optional keyword arguments.
+@end deffn
+
+@deffn {Procedure} source? obj
+Return @code{#t} if @var{obj} is an audio source object.
+@end deffn
+
+@deffn {Procedure} streaming-source? source
+Return @code{#t} if @var{source} contains streaming audio.
+@end deffn
+
+@deffn {Procedure} static-source? source
+Return @code{#t} if @var{source} contains static audio.
+@end deffn
+
+@deffn {Procedure} source-playing? source
+Return @code{#t} if @var{source} is currently playing.
+@end deffn
+
+@deffn {Procedure} source-paused? source
+Return @code{#t} if @var{source} is currently paused.
+@end deffn
+
+@deffn {Procedure} source-stopped? source
+Return @code{#t} if @var{source} is currently stopped.
+@end deffn
+
+@deffn {Procedure} source-pitch source
+Return the pitch multiplier of @var{source}.
+@end deffn
+
+@deffn {Procedure} source-volume source
+Return the volume multiplier of @var{source}.
+@end deffn
+
+@deffn {Procedure} source-min-volume source
+Return the minimum volume of @var{source}.
+@end deffn
+
+@deffn {Procedure} source-max-volume source
+Return the maximum volume of @var{source}.
+@end deffn
+
+@deffn {Procedure} source-max-distance source
+Return the maximum distance of @var{source}.
+@end deffn
+
+@deffn {Procedure} source-reference-distance source
+Return the reference distance of @var{source}.
+@end deffn
+
+@deffn {Procedure} source-rolloff-factor source
+Return the rolloff factor of @var{source}.
+@end deffn
+
+@deffn {Procedure} source-cone-outer-volume source
+Return the volume of @var{source} when outside the oriented cone.
+@end deffn
+
+@deffn {Procedure} source-cone-inner-angle source
+Return the inner angle of the sound cone of @var{source} in radians.
+@end deffn
+
+@deffn {Procedure} source-cone-outer-angle source
+Return the outer angle of the sound cone of @var{source} in radians.
+@end deffn
+
+@deffn {Procedure} source-position source
+Return the position of @var{source} as a 3D vector.
+@end deffn
+
+@deffn {Procedure} source-velocity source
+Return the velocity of @var{source} as a 3D vector.
+@end deffn
+
+@deffn {Procedure} source-direction source
+Return the direction of @var{source} as a 3D vector.
+@end deffn
+
+@deffn {Procedure} source-relative? source
+Return @code{#t} if the position of @var{source} is relative to the
+listener's position.
+@end deffn
+
+@deffn {Procedure} source-play source
+Begin/resume playback of @var{source}.
+@end deffn
+
+@deffn {Procedure} source-pause source
+Pause playback of @var{source}.
+@end deffn
+
+@deffn {Procedure} source-toggle source
+Play @var{source} if it is currently paused or pause @var{source} if
+it is currently playing.
+@end deffn
+
+@deffn {Procedure} source-stop [source]
+Stop playing @var{source} or, if no source is specified, stop playing
+@emph{all} sources.
+@end deffn
+
+@deffn {Procedure} source-rewind source
+Rewind @var{source} to the beginning of the audio stream.
+@end deffn
+
+@deffn {Procedure} set-source-audio! source audio
+Set the playback stream for @var{source} to @var{audio}.
+@end deffn
+
+@deffn {Procedure} set-source-loop! source loop?
+Configure whether or not @var{source} should loop the audio stream.
+@end deffn
+
+@deffn {Procedure} set-source-pitch! source pitch
+Set the pitch multiplier of @var{source} to @var{pitch}
+@end deffn
+
+@deffn {Procedure} set-source-volume! source volume
+Set the volume of @var{source} to @var{volume}. A value of 1.0 is
+100% volume.
+@end deffn
+
+@deffn {Procedure} set-source-min-volume! source volume
+Set the minimum volume of @var{source} to @var{volume}.
+@end deffn
+
+@deffn {Procedure} set-source-max-volume! source volume
+Set the maximum volume of @var{source} to @var{volume}.
+@end deffn
+
+@deffn {Procedure} set-source-max-distance! source distance
+Set the distance where there will no longer be any attenuation of
+@var{source} to @var{distance}.
+@end deffn
+
+@deffn {Procedure} set-source-reference-distance! source distance
+Set the reference distance of @var{source} to @var{distance}.
+@end deffn
+
+@deffn {Procedure} set-source-rolloff-factor! source factor
+Set the rolloff factor for @var{source} to @var{factor}.
+@end deffn
+
+@deffn {Procedure} set-source-cone-outer-volume! source volume
+Set the volume for @var{source} when outside the sound cone to @var{volume}.
+@end deffn
+
+@deffn {Procedure} set-source-cone-inner-angle! source angle
+Set the inner angle of the sound cone of @var{source} to @var{angle} radians.
+@end deffn
+
+@deffn {Procedure} set-source-cone-outer-angle! source angle
+Set the outer angle of the sound cone of @var{source} to @var{angle} radians.
+@end deffn
+
+@deffn {Procedure} set-source-position! source position
+Set the position of @var{source} to the 3D vector @var{position}.
+@end deffn
+
+@deffn {Procedure} set-source-velocity! source velocity
+Set the velocity of @var{source} to the 3D vector @var{velocity}.
+@end deffn
+
+@deffn {Procedure} set-source-direction! source direction
+Set the velocity of @var{source} to the 3D vector @var{direction}.
+@end deffn
+
+@deffn {Procedure} set-source-relative! source relative?
+If @var{relative?} is @code{#t}, the position of @var{source} is
+interpreted as relative to the listener's position. Otherwise, the
+position of @var{source} is in absolute coordinates.
+@end deffn
+
+@node The Listener
+@subsection The Listener
+
+The listener is a collection of global state that represents the
+player within the 3D sound model. For games that do not need 3D sound
+modeling, manipulating the listener's master volume is the only
+interesting thing to do here.
+
+@deffn {Procedure} listener-volume
+Return the current master volume of the listener.
+@end deffn
+
+@deffn {Procedure} listener-position
+Return the current position of the listener.
+@end deffn
+
+@deffn {Procedure} listener-velocity
+Return the current velocity of the listener.
+@end deffn
+
+@deffn {Procedure} listener-orientation
+Return the current orientation of the listener.
+@end deffn
+
+@deffn {Procedure} set-listener-volume! volume
+Set the listener's master volume to @var{volume}, a value in the range [0,
+1].
+@end deffn
+
+@deffn {Procedure} set-listener-position! position
+Set the listener's position to the 3D vector @var{position}.
+@end deffn
+
+@deffn {Procedure} set-listener-velocity! velocity
+Set the listener's velocity to the 3D vector @var{velocity}.
+@end deffn
+
+@deffn {Procedure} set-listener-orientation! at up
+Set the listener's orientation to the 3D vectors @var{at} and
+@var{up}.
+@end deffn
+
+@node The Environment
+@subsection The Environment
+
+The environment defines global parameters that govern how sound is
+processed within the 3D modeling space.
+
+@deffn {Procedure} doppler-factor
+Return the current doppler factor.
+@end deffn
+
+@deffn {Procedure} speed-of-sound
+Return the current speed of sound.
+@end deffn
+
+@deffn {Procedure} distance-model
+Return the current distance model.
+
+Possible return values are:
+
+@itemize
+@item @code{none}
+@item @code{inverse-distance}
+@item @code{inverse-distance-clamped} (the default)
+@item @code{linear-distance}
+@item @code{linear-distance-clamped}
+@item @code{exponent-distance}
+@item @code{exponent-distance-clamped}
+@end itemize
+
+@end deffn
+
+@deffn {Procedure} set-doppler-factor! doppler-factor
+Change the doppler factor to @var{doppler-factor}.
+@end deffn
+
+@deffn {Procedure} set-speed-of-sound! speed-of-sound
+Change the speed of sound to @var{speed-of-sound}.
+@end deffn
+
+@deffn {Procedure} set-distance-model! distance-model
+Change the distance model to @var{distance-model}. Valid distance
+models are:
+
+@itemize
+@item @code{none}
+@item @code{inverse-distance}
+@item @code{inverse-distance-clamped}
+@item @code{linear-distance}
+@item @code{linear-distance-clamped}
+@item @code{exponent-distance}
+@item @code{exponent-distance-clamped}
+@end itemize
+
+@end deffn
@node Scripting
@section Scripting