From 4c22dd54ccc213afb1a6bee047b7be1d17241999 Mon Sep 17 00:00:00 2001 From: Hans Dijkema Date: Wed, 22 Apr 2026 13:28:37 +0200 Subject: [PATCH] mp3 support, based on mpg123 --- audio-decoder.rkt | 35 ++++-- flac-decoder.rkt | 7 +- libflac-ffi.rkt | 16 ++- libmpg123-ffi.rkt | 234 +++++++++++++++++++++--------------- mp3-decoder.rkt | 128 ++++++++++++++++++++ play-test.rkt | 26 ++-- scrbl/audio-decoder.scrbl | 245 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 575 insertions(+), 116 deletions(-) create mode 100644 mp3-decoder.rkt create mode 100644 scrbl/audio-decoder.scrbl diff --git a/audio-decoder.rkt b/audio-decoder.rkt index 9a54039..f92c3d8 100644 --- a/audio-decoder.rkt +++ b/audio-decoder.rkt @@ -1,6 +1,7 @@ (module audio-decoder racket/base (require "flac-decoder.rkt" + "mp3-decoder.rkt" racket/contract racket/string racket/path @@ -24,11 +25,12 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (define-struct audio-reader - (exts valid? open reader seeker stopper)) + (exts valid? open reader seeker stopper ao-type)) ;; audiotype, audio-reader (define audio-readers (make-hash)) - + + ;; FLAC (hash-set! audio-readers 'flac (make-audio-reader '("flac") @@ -36,14 +38,26 @@ flac-open flac-read flac-seek - flac-stop)) + flac-stop + 'flac)) + + ;; MP3 + (hash-set! audio-readers + 'mp3 + (make-audio-reader '("mp3") + mp3-valid? + mp3-open + mp3-read + mp3-seek + mp3-stop + 'ao)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Known extensions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (define known-extensions - '("flac")) + '("flac" "mp3")) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Register audio reader @@ -89,12 +103,13 @@ (let ((reader (find-reader file))) (if (eq? reader #f) #f - (audio-reader-valid? file))) + ((audio-reader-valid? (cadr reader)) file))) #f)))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; cb-stream-info will be called with - ; - audio-type: symbol? + ; - audio-type: symbol? (e.g. 'flac, 'mp3) + ; - ao-type: symbol? - e.g. 'flac, 'ao (ao means the buffer can be used directly by aolib). ; - handle: audio-handle? ; - meta: hash? ; Meta information must at least contain: @@ -106,6 +121,7 @@ ; ; cb-audio will be called with ; - audio-type: symbol? + ; - ao-type: symbol? - e.g. 'flac, 'ao (ao means the buffer can be used directly by aolib). ; - handle: audio-handle? ; - buf-info: hash? ; - buffer: cpointer? - contains data to be fed to ao - must be owned / released by the driver @@ -134,6 +150,7 @@ (error (format "Cannot find reader for '~a'" audio-file))) (let* ((reader-type (car reader*)) (reader (cadr reader*)) + (ao-type (audio-reader-ao-type reader)) (handle (make-audio-handle reader-type cb-stream-info cb-audio reader #f)) ) (set-audio-handle-driver-handle! @@ -141,9 +158,9 @@ ((audio-reader-open reader) file (λ (meta) - (cb-stream-info reader-type handle meta)) + (cb-stream-info reader-type ao-type handle meta)) (λ (buf-info audio-buffer buf-len) - (cb-audio reader-type handle buf-info audio-buffer buf-len))) + (cb-audio reader-type ao-type handle buf-info audio-buffer buf-len))) ) handle) ) @@ -153,7 +170,7 @@ (define/contract (audio-read handle) (-> audio-handle? void?) (let ((reader (audio-reader-reader (audio-handle-driver handle)))) - (void? (reader (audio-handle-driver-handle handle))))) + (void (reader (audio-handle-driver-handle handle))))) (define/contract (audio-seek handle percentage) (-> audio-handle? number? void?) diff --git a/flac-decoder.rkt b/flac-decoder.rkt index abc5de7..fa8f04b 100644 --- a/flac-decoder.rkt +++ b/flac-decoder.rkt @@ -124,7 +124,12 @@ st) (reader (+ frame-nr 1)))))) )) - (reader 0)))) + (reader 0) + ; done reading, delete flac encoder + (ffi-handler 'delete) + ) + ) + ) (define (flac-read-meta handle) (let* ((ffi-handler (flac-handle-ffi-decoder-handler handle)) diff --git a/libflac-ffi.rkt b/libflac-ffi.rkt index a402459..fa7b4e3 100644 --- a/libflac-ffi.rkt +++ b/libflac-ffi.rkt @@ -520,10 +520,14 @@ ) (define (new) - (set! fl (FLAC__stream_decoder_new)) + (dbg-sound "flac-ffi 'new") + (if (eq? fl #f) + (set! fl (FLAC__stream_decoder_new)) + (error "flac handler already initialized (new)")) fl) (define (init file) + (dbg-sound "flac-ffi 'init") (let ((r (FLAC__stream_decoder_init_file fl file @@ -534,6 +538,15 @@ (set! flac-file file) r)) + (define (delete) + (dbg-sound "flac-ffi 'delete") + (if (eq? fl #f) + (error "flac handler has already been deleted") + (begin + (FLAC__stream_decoder_delete fl) + (set! fl #f))) + ) + (define (process-single) (FLAC__stream_decoder_process_single fl)) @@ -583,6 +596,7 @@ [(eq? cmd 'new) (new)] [(eq? cmd 'init) (init (car args))] + [(eq? cmd 'delete) (delete)] [(eq? cmd 'process-single) (process-single)] [(eq? cmd 'get-buffers) (buffer->vectorlist (car args) (cadr args) (caddr args))] diff --git a/libmpg123-ffi.rkt b/libmpg123-ffi.rkt index b31fccb..14afefa 100644 --- a/libmpg123-ffi.rkt +++ b/libmpg123-ffi.rkt @@ -43,7 +43,7 @@ MPG123_NEED_MORE = -10 ;/**< Message: For feed reader: "Feed me more!" (call ; mpg123_feed() or mpg123_decode() with some new input data). */ MPG123_ERR = -1 ;/**< Generic Error */ - MPG123_OK=0 ;/**< Success */ + MPG123_OK = 0 ;/**< Success */ MPG123_BAD_OUTFORMAT ;/**< Unable to set up output format! */ MPG123_BAD_CHANNEL ;/**< Invalid channel number specified. */ MPG123_BAD_RATE ;/**< Invalid sample rate specified. */ @@ -91,6 +91,7 @@ MPG123_INT_OVERFLOW ;/**< Some integer overflow. */ MPG123_BAD_FLOAT ;/**< Floating-point computations work not as expected. */ ) + _int ) ) @@ -98,6 +99,7 @@ ; MPG123_EXPORT int mpg123_init (void) + ; Not relevant anymore (define-libmpg123 mpg123_init (_fun -> _MPG123_Result)) @@ -157,7 +159,26 @@ (define-libmpg123 mpg123_length64 (_fun _mpg123_handle -> _int64)) + ; MPG123_EXPORT void mpg123_delete (mpg123_handle *mh) + (define-libmpg123 mpg123_delete + (_fun _mpg123_handle -> _void)) + ; MPG123_EXPORT void mpg123_exit (void) + ; Not relevant anymore + (define-libmpg123 mpg123_exit + (_fun -> _void)) + + ; MPG123_EXPORT const char* mpg123_plain_strerror ( int errcode ) + (define-libmpg123 mpg123_plain_strerror + (_fun _MPG123_Result -> _string*/utf-8)) + + (define mpg123_int_strerror + (get-ffi-obj "mpg123_plain_strerror" + lib + (_fun _int -> _string*/utf-8))) + + + #| #include #include @@ -222,112 +243,135 @@ int main(int argc, char *argv[]) ;; Our interface for decoding to racket ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(define (mpg123-ffi-decoder-handler) - #t -#| (define write-data '()) - (define meta-data '()) - (define error-no -1) - (define fl #f) - (define flac-file #f) - (define client-data #f) - - (define (write-callback fl frame buffer client-data) - (set! write-data (append write-data (list (cons frame buffer)))) - 0) + (define BITS 8) - (define (meta-callback fl meta client-data) - (let ((meta-clone (FLAC__metadata_object_clone meta))) - (unless (eq? meta-clone #f) - (set! meta-data (append meta-data (list meta-clone)))))) + (define (mpg123-ffi-decoder-handler) - (define (error-callback fl errno client-data) - (set! error-no errno) - ) + (define mh #f) - (define (new) - (set! fl (FLAC__stream_decoder_new)) - fl) + (define buf-size -1) + (define buffer #f) - (define (init file) - (let ((r (FLAC__stream_decoder_init_file - fl - file - write-callback - meta-callback - error-callback - client-data))) - (set! flac-file file) - r)) + (define rate -1) + (define channels -1) + (define sample-bits -1) + (define sample-bytes -1) + (define pcm-length -1) + (define encoding -1) + (define mp3-file "") - (define (process-single) - (FLAC__stream_decoder_process_single fl)) + (define (new) + (if (eq? mh #f) + (let-values ([(h err) (mpg123_new #f)]) + (when (eq? h #f) + (error (format "mpg123_new: ~a" (mpg123_int_strerror err)))) + (set! mh h) + (set! buf-size (mpg123_outblock mh)) + (set! buffer (malloc buf-size 'atomic-interior )) + ) + (error "mpg123 handle already initialized, delete it first")) + #t) - (define (int-state) - (FLAC__stream_decoder_get_state fl)) - - (define (state) - (decoder-state (int-state))) + (define (delete) + (if (eq? mh #f) + (error "mpg123 has already been deleted") + (begin + (mpg123_delete mh) + (set! mh #f) + (set! buf-size -1) + (set! buffer #f) + )) + #t) - (define (process-meta-data cb) - (for-each (λ (meta-entry) - (cb meta-entry) - (FLAC__metadata_object_delete meta-entry)) - meta-data) - (set! meta-data '())) + (define (info) + (info-sound "file : ~a" mp3-file) + (info-sound "buf-size : ~a" buf-size) + (info-sound "channels : ~a" channels) + (info-sound "sample-bits: ~a" sample-bits) + (info-sound "rate : ~a" rate) + (info-sound "encoding : ~a" encoding) + (info-sound "pcm-length : ~a" pcm-length) + (info-sound "duration : ~a" (if (= rate -1) + 0 + (exact->inexact + (/ pcm-length rate)))) + #t + ) - (define (process-write-data cb) - (for-each (lambda (d) - (cb (car d) (cdr d))) - write-data) - (set! write-data '())) + (define (init file) + (let ((r (mpg123_open mh file))) + (unless (eq? r 'MPG123_OK) + (error (format "mpg123_open: ~a" (mpg123_plain_strerror r)))) + ) + (let-values ([(fr rate* channels* encoding*) (mpg123_getformat mh)]) + (unless (eq? fr 'MPG123_OK) + (error (format "mpg123_format: ~a" (mpg123_plain_strerror fr)))) + (set! rate rate*) + (set! channels channels*) + (set! encoding encoding*) + (dbg-sound "mpg123_format: ~a ~a ~a" rate channels encoding) + (set! sample-bits (* (mpg123_encsize encoding) BITS)) + (set! sample-bytes (/ sample-bits 8))) + (let ((sr (mpg123_scan mh))) + (unless (eq? sr 'MPG123_OK) + (error (format "mpg123_scan: ~a" (mpg123_plain_strerror sr)))) + (set! pcm-length (mpg123_length64 mh))) + (set! mp3-file (format "~a" file)) + #t) - (define (buffer->vectorlist buffer channels size) - (letrec ((for-channels - (lambda (channel) - (if (< channel channels) - (letrec ((v (make-vector size 0)) - (p (ptr-ref buffer FLAC__int32-pointer channel)) - (to-vec (lambda (i) - (when (< i size) - (vector-set! v i (ptr-ref p _int32 i)) - (to-vec (+ i 1))))) - ) - (to-vec 0) - (cons v (for-channels (+ channel 1)))) - '()))) - ) - (for-channels 0))) + (define (mp3-format cb) + (cb rate channels sample-bits sample-bytes pcm-length)) - (define (seek-to-sample sample) - (FLAC__stream_decoder_seek_absolute fl sample)) + (define (close) + (let ((r (mpg123_close mh))) + (unless (eq? r 'MPG123_OK) + (error (format "mpg123_close: ~a" (mpg123_plain_strerror r)))) + (set! channels -1) + (set! pcm-length -1) + (set! rate -1) + (set! sample-bits -1) + (set! sample-bytes -1) + (set! encoding -1) + (set! mp3-file "") + #t)) - (lambda (cmd . args) - (cond - [(eq? cmd 'write-data) write-data] - [(eq? cmd 'meta-data) meta-data] - - [(eq? cmd 'new) (new)] - [(eq? cmd 'init) (init (car args))] - [(eq? cmd 'process-single) (process-single)] - [(eq? cmd 'get-buffers) (buffer->vectorlist (car args) (cadr args) (caddr args))] - - [(eq? cmd 'int-state) (int-state)] - [(eq? cmd 'state) (state)] - - [(eq? cmd 'has-write-data?) (not (null? write-data))] - [(eq? cmd 'has-meta-data?) (not (null? meta-data))] - [(eq? cmd 'has-errno?) (not (= error-no -1))] - - [(eq? cmd 'process-meta-data) (process-meta-data (car args))] - [(eq? cmd 'process-write-data) (process-write-data (car args))] - [(eq? cmd 'errno) error-no] + (define (read cb) + (let-values ([(r done) (mpg123_read mh buffer buf-size)]) + (if (eq? r 'MPG123_DONE) + (cb 'done -1 buffer done) + (if (eq? r 'MPG123_OK) + (let ((pcm-pos (mpg123_tell64 mh))) + (cb 'data pcm-pos buffer done)) + (error (format "mpg123_read: ~a" (mpg123_plain_strerror r))) + ) + ) + ) + #t) - [(eq? cmd 'seek-to-sample) (seek-to-sample (car args))] - [(eq? cmd 'file) flac-file] - - [else (error (format "unknown command ~a" cmd))] - )) - |# + (define (seek pcm-pos) + (let ((r (mpg123_seek64 mh pcm-pos 'seek-set))) + (unless (>= r 0) + (error (format "mpg123_seek64: ~a" (mpg123_int_strerror r)))) + #t)) + + (define (tell) + (mpg123_tell64 mh)) + + (λ (cmd . args) + (cond + [(eq? cmd 'new) (new)] + [(eq? cmd 'delete) (delete)] + [(eq? cmd 'init) (init (car args))] + [(eq? cmd 'close) (close)] + [(eq? cmd 'format) (mp3-format (car args))] + [(eq? cmd 'info) (info)] + [(eq? cmd 'read) (read (car args))] + [(eq? cmd 'seek) (seek (car args))] + [(eq? cmd 'tell) (tell)] + [(eq? cmd 'file) mp3-file] + [else (error (format "Unknown command: ~a" cmd))] + ) + ) ) ); end of module \ No newline at end of file diff --git a/mp3-decoder.rkt b/mp3-decoder.rkt new file mode 100644 index 0000000..ffeb3d3 --- /dev/null +++ b/mp3-decoder.rkt @@ -0,0 +1,128 @@ +(module mp3-decoder racket/base + + (require ffi/unsafe + "libmpg123-ffi.rkt" + "private/utils.rkt") + + (provide mp3-open + mp3-valid? + mp3-read + mp3-stop + mp3-seek + ) + + + (define-struct mp3-handle + (if cb-info cb-audio + (stop #:mutable) + (seek #:mutable) + (reading #:mutable) + (format #:mutable) + ) + #:transparent + ) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Functions to do the good stuff +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + (define (mp3-valid? mp3-file) + #t) + + (define audio-type 'mp3) + + (define (report-format handle current-pcm-pos) + ((mp3-handle-cb-info handle) (mp3-handle-format handle))) + + (define (give-audio handle info pos buffer size) + (let ((h (mp3-handle-format handle))) + (hash-set! h 'sample pos) + (hash-set! h 'current-time (exact->inexact (/ pos (hash-ref h 'sample-rate)))) + ((mp3-handle-cb-audio handle) h buffer size))) + + (define (mp3-open mp3-file* cb-stream-info cb-audio) + (let ((mp3-file (if (path? mp3-file*) (path->string mp3-file*) mp3-file*))) + (if (file-exists? mp3-file) + (let ((handler (mpg123-ffi-decoder-handler))) + (handler 'new) + (handler 'init mp3-file) + (let ((h (make-mp3-handle handler + cb-stream-info + cb-audio + #f + #f + #f + #f + ))) + (handler 'format + (λ (rate channels sample-bits sample-bytes pcm-length) + (let ((f (make-hash))) + (hash-set! f 'duration (exact->inexact (/ pcm-length rate))) + (hash-set! f 'sample-rate rate) + (hash-set! f 'channels channels) + (hash-set! f 'bits-per-sample sample-bits) + (hash-set! f 'bytes-per-sample sample-bytes) + (hash-set! f 'total-samples pcm-length) + (set-mp3-handle-format! h f) + ) + ) + ) + (report-format h 0) + h)) + #f))) + + + (define (mp3-read handle) + (let* ((ffi-handler (mp3-handle-if handle)) + (cb-info (mp3-handle-cb-info handle)) + (cb-audio (mp3-handle-cb-audio handle)) + ) + (set-mp3-handle-reading! handle #t) + (let loop () + (if (eq? (mp3-handle-stop handle) #t) + (begin + (newline) + (dbg-sound "Stopping mp3 decoding") + (set-mp3-handle-reading! handle #f) + 'stopped-reading + ) + (begin + (unless (eq? (mp3-handle-seek handle) #f) + (dbg-sound "Seeking to ~a" (mp3-handle-seek handle)) + (ffi-handler 'seek (mp3-handle-seek handle)) + (set-mp3-handle-seek! handle #f)) + (ffi-handler 'read + (λ (info pos buffer size) + (if (eq? info 'done) + (set-mp3-handle-stop! handle #t) + (give-audio handle info pos buffer size) + )) + ) + (loop) + ) + )) + (ffi-handler 'close) + (ffi-handler 'delete) + ) + ) + + (define (mp3-seek handle percentage) + (let ((fmt (mp3-handle-format handle))) + (let ((total-samples (hash-ref fmt 'total-samples))) + (unless (or + (eq? total-samples #f) + (= total-samples -1)) + (let ((sample (inexact->exact (round * (exact->inexact (/ percentage 100.0)) total-samples)))) + ((mp3-handle-if handle) 'seek sample)) + ) + ) + ) + ) + + (define (mp3-stop handle) + (set-mp3-handle-stop! handle #t) + (while (mp3-handle-reading handle) + (sleep 0.01)) + ) + + ); end of module diff --git a/play-test.rkt b/play-test.rkt index bf01e45..e1c4289 100644 --- a/play-test.rkt +++ b/play-test.rkt @@ -3,6 +3,7 @@ "audio-decoder.rkt" simple-log "private/utils.rkt" + racket-sprintf ;data/queue ;racket-sound ) @@ -11,7 +12,9 @@ (define test-file3-id 3) (let ((os (system-type 'os))) (when (eq? os 'unix) - (set! test-file3 "/muziek/Klassiek-Viool/Alina Ibragimova/Paganini_24 Caprices (2021)/24. 24 Caprices, Op 1 - No. 24 in A minor- Tema con variazioni. Quasi presto.flac")) + ;(set! test-file3 "/muziek/Klassiek-Viool/Alina Ibragimova/Paganini_24 Caprices (2021)/24. 24 Caprices, Op 1 - No. 24 in A minor- Tema con variazioni. Quasi presto.flac") + (set! test-file3 "/tmp/test.mp3") + ) (when (eq? os 'windows) (set! test-file3 "C:\\Muziek\\Klassiek-Strijkkwartet\\Quatuor Zaïde\\Franz\\01 Erlkönig, D. 328 (Arr. For String Quartet by Eric Mouret).flac") ;(set! test-file3 "C:\\Muziek\\Klassiek-Viool\\Janine Jansen\\Janine Jansen - Sibelius en Prokovief 1 (2024)\\02 - Violin Concerto in D Minor, Op. 47 II. Adagio di molto.flac") @@ -26,7 +29,7 @@ (sl-log-to-display) -(define (audio-play type handle buf-info buffer buf-len) +(define (audio-play type ao-type handle buf-info buffer buf-len) (let* ((sample (hash-ref buf-info 'sample)) (rate (hash-ref buf-info 'sample-rate)) (second (/ (* sample 1.0) (* rate 1.0))) @@ -36,11 +39,12 @@ (bytes-per-sample-all-channels (* channels bytes-per-sample)) (duration (hash-ref buf-info 'duration)) ) - (displayln buf-info) + ;(displayln buf-info) (when (eq? ao-h #f) - (set! ao-h (ao-open-live bits-per-sample rate channels 'big-endian))) + (set! ao-h (ao-open-live bits-per-sample rate channels 'native-endian))) ;(displayln 'ao-play) - (ao-play ao-h test-file3-id second duration buffer buf-len type) + (ao-play ao-h test-file3-id second duration buffer buf-len ao-type) + (set! duration (inexact->exact (round duration))) ;(displayln 'done) (let ((second-printer (λ (buf-seconds) (let ((s (inexact->exact (round (ao-at-second ao-h))))) @@ -52,7 +56,8 @@ (tseconds (remainder duration 60)) (volume (ao-volume ao-h)) ) - (displayln (format "At time: ~a:~a (~a:~a) - ~a - volume: ~a" + (info-sound + (sprintf "At time: %02d:%02d (%02d:%02d) - %d - volume: %d" minutes seconds tminutes tseconds buf-seconds @@ -68,7 +73,7 @@ bytes-per-sample-all-channels rate)))) (if (< buf-seconds-left 2.0) - (displayln (format "Seconds in buffer left: ~a" buf-seconds-left)) + (info-sound "Seconds in buffer left: ~a" buf-seconds-left) (begin (sleep 0.5) (second-printer buf-seconds) @@ -89,9 +94,10 @@ ) ) -(define (audio-meta type handle meta) - (dbg-sound "type: ~a" type) - (dbg-sound "meta: ~a" meta)) +(define (audio-meta type ao-type handle meta) + (dbg-sound "type: ~a" type) + (dbg-sound "ao-type: ~a" ao-type) + (dbg-sound "meta: ~a" meta)) (define (play) (set! ao-h #f) diff --git a/scrbl/audio-decoder.scrbl b/scrbl/audio-decoder.scrbl new file mode 100644 index 0000000..19ec2ea --- /dev/null +++ b/scrbl/audio-decoder.scrbl @@ -0,0 +1,245 @@ +#lang scribble/manual + +@(require racket/base + (for-label racket/base + racket/path + "../audio-decoder.rkt")) + +@title{audio-decoder} +@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]] + +@defmodule["audio-decoder.rkt"] + +This module provides a small abstraction layer over concrete audio +decoders. A decoder backend is selected from the filename extension +and is then used through a uniform interface for opening, reading, +seeking, and stopping. + +The module also allows additional decoder backends to be registered +with @racket[audio-register-reader!]. + +@section{Reader registration} + +A reader descriptor stores the extensions handled by a backend +together with the procedures used to validate, open, read, seek, +and stop that backend. + +@defproc[(make-audio-reader [exts (listof string?)] + [valid? procedure?] + [open procedure?] + [reader procedure?] + [seeker procedure?] + [stopper procedure?]) + struct?]{ + +Creates a reader descriptor. + +The @racket[exts] list contains the filename extensions handled by the +reader, without a leading dot. Matching is case-insensitive. + +The procedures are used as follows: + +@itemlist[#:style 'compact + @item{@racket[valid?] checks whether a file is valid for this reader;} + @item{@racket[open] opens a decoder for a file;} + @item{@racket[reader] reads or continues decoding;} + @item{@racket[seeker] seeks within the audio stream;} + @item{@racket[stopper] stops an active decode loop.}] + +The built-in FLAC backend is registered in this way. +} + +@defproc[(audio-register-reader! [type symbol?] + [reader struct?]) + void?]{ + +Registers @racket[reader] under @racket[type]. + +The extensions declared in @racket[reader] are appended to the list +returned by @racket[audio-known-exts?], and the reader becomes +available to @racket[audio-open]. +} + +@section{Audio handles} + +@defproc[(audio-handle? [v any/c]) boolean?]{ + +Returns @racket[#t] if @racket[v] is an audio handle, and @racket[#f] +otherwise. +} + +@defproc[(audio-kind [handle audio-handle?]) symbol?]{ + +Returns the reader type stored in @racket[handle]. + +For the built-in FLAC backend this value is @racket['flac]. +} + +@section{Known extensions and validation} + +@defproc[(audio-known-exts?) (listof string?)]{ + +Returns the list of known filename extensions. + +The initial list contains @racket["flac"]. Additional extensions are +added when readers are registered with +@racket[audio-register-reader!]. +} + +@defproc[(audio-valid-ext? [ext any/c]) boolean?]{ + +Returns @racket[#t] if @racket[ext] denotes a known filename +extension, and @racket[#f] otherwise. + +The argument is first converted to a string. If it starts with a dot, +that dot is removed. Matching is case-insensitive. +} + +@defproc[(audio-file-valid? [file (or/c string? path?)]) boolean?]{ + +Returns @racket[#t] if @racket[file] has a known extension and matches +a registered reader, and @racket[#f] otherwise. + +This procedure first derives the filename extension and checks it with +@racket[audio-valid-ext?]. If the extension is known, it then looks up +the matching reader and performs the reader-specific validity check. +} + +@section{Opening and callbacks} + +@defproc[(audio-open [audio-file (or/c string? path?)] + [cb-stream-info procedure?] + [cb-audio procedure?]) + audio-handle?]{ + +Opens an audio decoder for @racket[audio-file]. + +If @racket[audio-file] is a path, it is converted to a string before +it is passed to the backend open procedure. + +This procedure raises an exception if the file is not considered a +valid audio file, if the file does not exist, or if no registered +reader can be found for the file. + +The returned handle stores the selected reader type, the callback +procedures, the reader descriptor, and the driver-specific handle +returned by the backend open procedure. + +The callback procedures are wrapped before they are passed to the +backend. + +The stream-info callback is called as: + +@racketblock[ +(cb-stream-info audio-type handle meta) +] + +where: + +@itemlist[#:style 'compact + @item{@racket[audio-type] is the registered reader type, such as + @racket['flac];} + @item{@racket[handle] is the generic @racket[audio-handle];} + @item{@racket[meta] is a hash table with stream metadata.}] + +According to the source comments, @racket[meta] must contain at least: + +@itemlist[#:style 'compact + @item{@racket['duration] --- duration of the audio in seconds, possibly + fractional;} + @item{@racket['bits-per-sample] --- number of audio bits per sample;} + @item{@racket['channels] --- number of audio channels;} + @item{@racket['sample-rate] --- number of samples per second per + channel;} + @item{@racket['total-samples] --- total number of samples in the + audio.}] + +The audio callback is called as: + +@racketblock[ +(cb-audio audio-type handle buf-info buffer buf-size) +] + +where: + +@itemlist[#:style 'compact + @item{@racket[audio-type] is the registered reader type;} + @item{@racket[handle] is the generic @racket[audio-handle];} + @item{@racket[buf-info] is a hash table describing the audio buffer;} + @item{@racket[buffer] is a native buffer containing audio data;} + @item{@racket[buf-size] is the size of that buffer in bytes.}] + +According to the source comments, the buffer is to be owned and +released by the decoder driver. The comments also note that the +@tt{ao-async} backend copies the data. + +According to the source comments, @racket[buf-info] must contain at +least: + +@itemlist[#:style 'compact + @item{@racket['duration] --- duration of the audio in seconds, possibly + fractional;} + @item{@racket['bits-per-sample] --- number of audio bits per sample;} + @item{@racket['channels] --- number of audio channels;} + @item{@racket['sample-rate] --- number of samples per second per + channel;} + @item{@racket['total-samples] --- total number of samples in the + audio;} + @item{@racket['sample] --- the current sample to which the audio + buffer applies.}] +} + +@section{Reading, seeking, and stopping} + +@defproc[(audio-read [handle audio-handle?]) void?]{ + +Calls the registered reader procedure for @racket[handle]. + +The concrete reader procedure receives the driver-specific handle +stored in the generic audio handle. Any result value produced by the +backend is discarded. +} + +@defproc[(audio-seek [handle audio-handle?] + [percentage number?]) + void?]{ + +Calls the registered seek procedure for @racket[handle]. + +The @racket[percentage] argument is passed unchanged to the backend +seek procedure. + +In this abstraction layer, the parameter represents a relative +position in the full audio stream. A backend registered through +@racket[audio-register-reader!] is expected to follow that +interpretation. +} + +@defproc[(audio-stop [handle audio-handle?]) void?]{ + +Calls the registered stop procedure for @racket[handle]. + +The concrete stop procedure receives the driver-specific handle stored +in the generic audio handle. +} + +@section{Using custom decoders} + +Custom audio decoders can be integrated by constructing a reader +descriptor with @racket[make-audio-reader] and registering it with +@racket[audio-register-reader!]. + +A backend integrated through this interface should provide: + +@itemlist[#:style 'compact + @item{a list of handled filename extensions;} + @item{a file-validity procedure;} + @item{an open procedure that accepts a file path, a stream-info + callback, and an audio callback;} + @item{a read procedure that accepts the driver-specific handle;} + @item{a seek procedure that accepts the driver-specific handle and a + numeric relative position;} + @item{a stop procedure that accepts the driver-specific handle.}] + +Once registered, files with matching extensions can be opened through +@racket[audio-open] in the same way as the built-in FLAC backend. \ No newline at end of file