mp3 support, based on mpg123

This commit is contained in:
2026-04-22 13:28:37 +02:00
parent bba44733ab
commit 4c22dd54cc
7 changed files with 575 additions and 116 deletions
+26 -9
View File
@@ -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?)
+6 -1
View File
@@ -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))
+15 -1
View File
@@ -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))]
+139 -95
View File
@@ -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 <ao/ao.h>
#include <mpg123.h>
@@ -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
+128
View File
@@ -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
+16 -10
View File
@@ -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)
+245
View File
@@ -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.