Files

281 lines
10 KiB
Racket

(module audio-decoder racket/base
(require "flac-decoder.rkt"
"mp3-decoder.rkt"
"opusfile-decoder.rkt"
"ffmpeg-decoder.rkt"
"audio-sniffer.rkt"
"private/utils.rkt"
racket/contract
racket/string
racket/path
)
(provide audio-open
audio-read
audio-stop
audio-seek
audio-kind
audio-valid-ext?
audio-file-valid?
audio-known-exts?
audio-register-reader!
make-audio-reader
audio-handle?
audio-supported-extensions
current-opusfile-output-format
opusfile-output-format?
)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Audio readers
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define-struct audio-reader
(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")
flac-valid?
flac-open
flac-read
flac-seek
flac-stop
'ao))
;; MP3
(hash-set! audio-readers
'mp3
(make-audio-reader '("mp3")
mp3-valid?
mp3-open
mp3-read
mp3-seek
mp3-stop
'ao))
;; Opus, via Xiph libopusfile
(hash-set! audio-readers
'opusfile
(make-audio-reader '("opus")
opusfile-valid?
opusfile-open
opusfile-read
opusfile-seek
opusfile-stop
'ao))
;; FFmpeg decoder
(hash-set! audio-readers
'ffmpeg
(make-audio-reader '("ogg" "oga" "opus"
"m4a" "mp4" "m4b"
"aac"
"wav"
"aiff" "aif" "aifc"
"wma"
"webm" "mkv" "mka")
ffmpeg-valid?
ffmpeg-open
ffmpeg-read
ffmpeg-seek
ffmpeg-stop
'ao))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Known extensions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define known-extensions
'("flac" ; FLAC decoder
"mp3" ; mp3 decoder
"ogg" "oga" "opus"
"m4a" "mp4" "m4b"
"aac"
"wav"
"aiff" "aif" "aifc"
"wma"
"webm" "mkv" "mka" ; FFMPEG decoder
))
(define (audio-supported-extensions)
known-extensions)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Register audio reader
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define/contract (audio-register-reader! type reader)
(-> symbol? audio-reader? void?)
(set! known-extensions (append known-extensions (audio-reader-exts reader)))
(hash-set! audio-readers type reader))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Generic audio reader
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define-struct audio-handle
((kind #:mutable)
(cb-stream-info #:mutable)
(cb-audio #:mutable)
(driver #:mutable)
(driver-handle #:mutable)
)
#:transparent
)
(define (audio-known-exts?)
known-extensions)
(define/contract (audio-kind handle)
(-> audio-handle? symbol?)
(audio-handle-kind handle))
(define (audio-valid-ext? ext)
(set! ext (format "~a" ext))
(when (string-prefix? ext ".")
(set! ext (substring ext 1)))
(if (not (null? (filter (λ (e) (string-ci=? ext e)) known-extensions)))
#t
(begin
(warn-sound "extension '~a' not in known-extensions '~a'" ext known-extensions)
#f))
)
(define/contract (audio-file-valid? file)
(-> (or/c string? path?) boolean?)
(let ((f (build-path file)))
(let ((e (format "~a" (path-get-extension f))))
(if (audio-valid-ext? e)
(let ((reader (find-reader file)))
(if (eq? reader #f)
#f
((audio-reader-valid? (cadr reader)) file)))
#f))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; cb-stream-info will be called with
; - 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:
; ('duration . seconds) - duration of the audio in seconds (or fractions): double
; ('bits-per-sample . integer) - number of audio bits per sample
; ('channels . integer) - number of audio channels
; ('sample-rate . integer) - number of samples per second per channel
; ('total-samples . integer) - total number of samples of the audio
;
; 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
; the ao-async backend will copy the data
; - buf-size: integer? - contains the size of the data
; buf-info must at least contain:
; ('duration . seconds) - duration of the audio in seconds (or fractions): double
; ('bits-per-sample . integer) - number of audio bits per sample
; ('channels . integer) - number of audio channels
; ('sample-rate . integer) - number of samples per second per channel
; ('total-samples . integer) - total number of samples of the audio
; (sample . integer) - the current sample the audio buffer applies to.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define/contract (audio-open audio-file cb-stream-info cb-audio)
(-> (or/c string? path?) procedure? procedure? audio-handle?)
(let ((file (if (path? audio-file)
(path->string audio-file)
audio-file)))
(unless (audio-file-valid? audio-file)
(error (format "Not a valid audio file '~a'" audio-file)))
(unless (file-exists? audio-file)
(error (format "File '~a' does not exist" audio-file)))
(let ((reader* (find-reader audio-file)))
(when (eq? reader* #f)
(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!
handle
((audio-reader-open reader)
file
(λ (meta)
(cb-stream-info reader-type ao-type handle meta))
(λ (buf-info audio-buffer buf-len)
(cb-audio reader-type ao-type handle buf-info audio-buffer buf-len)))
)
handle)
)
)
)
(define/contract (audio-read handle)
(-> audio-handle? void?)
(let ((reader (audio-reader-reader (audio-handle-driver handle))))
(void (reader (audio-handle-driver-handle handle)))))
(define/contract (audio-seek handle percentage)
(-> audio-handle? number? void?)
(let ((seeker (audio-reader-seeker (audio-handle-driver handle))))
(void (seeker (audio-handle-driver-handle handle) percentage))))
(define/contract (audio-stop handle)
(-> audio-handle? void?)
(dbg-sound "audio-stop called")
(let ((stopper (audio-reader-stopper (audio-handle-driver handle))))
(void (stopper (audio-handle-driver-handle handle)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Utils
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (right-reader? reader ext)
(not (null? (filter (λ (e) (string-ci=? ext e)) (audio-reader-exts reader)))))
(define reader-for-kind
(make-hash
(list (cons 'mp3 'ffmpeg) ; ffmpeg does a better job on gapless playback...
(cons 'flac 'flac)
(cons 'ogg 'ffmpeg)
(cons 'vorbis 'ffmpeg)
(cons 'opus (if (opusfile-available?) 'opusfile 'ffmpeg))
(cons 'wav 'ffmpeg)
(cons 'aiff 'ffmpeg)
(cons 'mp4 'ffmpeg)
(cons 'aac 'ffmpeg)
(cons 'alac 'ffmpeg)
(cons 'ac3 'ffmpeg)
(cons 'ape 'ffmpeg)
(cons 'wavpack 'ffmpeg)
(cons 'wma 'ffmpeg)
(cons 'matroska 'ffmpeg))))
(define (find-reader audio-file)
; First try to sniff the format
(let ((format (audio-sniff-format/extension audio-file)))
(let ((reader-kind (hash-ref reader-for-kind format #f)))
(if (eq? reader-kind #f)
#f
(let ((reader (hash-ref audio-readers reader-kind)))
(list reader-kind reader))
)
)
)
)
) ; end of module