301 lines
11 KiB
Racket
301 lines
11 KiB
Racket
(module opusfile-decoder racket/base
|
|
|
|
(require ffi/unsafe
|
|
"private/utils.rkt")
|
|
|
|
(provide opusfile-open
|
|
opusfile-valid?
|
|
opusfile-read
|
|
opusfile-stop
|
|
opusfile-seek
|
|
opusfile-available?
|
|
current-opusfile-output-format
|
|
opusfile-output-format?)
|
|
|
|
;; Xiph libopusfile backend for Ogg Opus streams.
|
|
;;
|
|
;; By default this backend uses op_read(), which returns signed 16-bit
|
|
;; interleaved PCM. That is the most efficient path for direct libao
|
|
;; playback. For users who prefer the wider decoder output path, set
|
|
;; current-opusfile-output-format to 's24. In that mode the backend uses
|
|
;; op_read_float() and converts the interleaved float output to packed signed
|
|
;; 24-bit PCM in native byte order.
|
|
;;
|
|
;; Opus decode output is always 48 kHz PCM. The original input rate, if
|
|
;; present in metadata, is not the actual decoder output rate.
|
|
|
|
(define libopusfile
|
|
(with-handlers ([exn:fail? (lambda (_) #f)])
|
|
(ffi-lib "libopusfile" '("0" #f))))
|
|
|
|
(define _OggOpusFile _pointer)
|
|
|
|
(define default-frames-per-read 4096)
|
|
(define opus-sample-rate 48000)
|
|
|
|
(define (opusfile-output-format? v)
|
|
(or (eq? v 's16) (eq? v 's24)))
|
|
|
|
(define current-opusfile-output-format
|
|
(make-parameter 's16
|
|
(lambda (v)
|
|
(unless (opusfile-output-format? v)
|
|
(raise-argument-error 'current-opusfile-output-format "(or/c 's16 's24)" v))
|
|
v)))
|
|
|
|
(define (opus-bits-per-sample)
|
|
(case (current-opusfile-output-format)
|
|
[(s16) 16]
|
|
[(s24) 24]))
|
|
|
|
(define (opus-bytes-per-sample)
|
|
(case (current-opusfile-output-format)
|
|
[(s16) 2]
|
|
[(s24) 3]))
|
|
|
|
(define (ffi-proc name type)
|
|
(and libopusfile
|
|
(with-handlers ([exn:fail? (lambda (_) #f)])
|
|
(get-ffi-obj name libopusfile type))))
|
|
|
|
(define op_open_file
|
|
(ffi-proc "op_open_file"
|
|
(_fun _path (err : (_ptr o _int))
|
|
-> (r : _OggOpusFile)
|
|
-> (values r err))))
|
|
|
|
(define op_free
|
|
(ffi-proc "op_free"
|
|
(_fun _OggOpusFile -> _void)))
|
|
|
|
(define op_channel_count
|
|
(ffi-proc "op_channel_count"
|
|
(_fun _OggOpusFile _int -> _int)))
|
|
|
|
(define op_pcm_total
|
|
(ffi-proc "op_pcm_total"
|
|
(_fun _OggOpusFile _int -> _int64)))
|
|
|
|
(define op_pcm_seek
|
|
(ffi-proc "op_pcm_seek"
|
|
(_fun _OggOpusFile _int64 -> _int)))
|
|
|
|
(define op_read
|
|
(ffi-proc "op_read"
|
|
(_fun _OggOpusFile _bytes _int (li : (_ptr o _int))
|
|
-> (r : _int)
|
|
-> (values r li))))
|
|
|
|
(define op_read_float
|
|
(ffi-proc "op_read_float"
|
|
(_fun _OggOpusFile _pointer _int (li : (_ptr o _int))
|
|
-> (r : _int)
|
|
-> (values r li))))
|
|
|
|
(define (opusfile-available?)
|
|
(and libopusfile
|
|
op_open_file
|
|
op_free
|
|
op_channel_count
|
|
op_pcm_total
|
|
op_pcm_seek
|
|
op_read
|
|
op_read_float
|
|
#t))
|
|
|
|
(define-struct opusfile-handle
|
|
(of cb-info cb-audio
|
|
(stop #:mutable)
|
|
(seek #:mutable)
|
|
(reading #:mutable)
|
|
(format #:mutable)
|
|
(pcm-pos #:mutable))
|
|
#:transparent)
|
|
|
|
(define (raise-opus who fmt . args)
|
|
(apply error who fmt args))
|
|
|
|
(define (check-libopusfile who)
|
|
(unless (opusfile-available?)
|
|
(raise-opus who "libopusfile could not be loaded")))
|
|
|
|
(define (correct-format-hash h)
|
|
(unless (hash-ref h 'sample-rate #f)
|
|
(hash-set! h 'sample-rate opus-sample-rate))
|
|
(unless (hash-ref h 'bits-per-sample #f)
|
|
(hash-set! h 'bits-per-sample (opus-bits-per-sample)))
|
|
(unless (hash-ref h 'bytes-per-sample #f)
|
|
(hash-set! h 'bytes-per-sample (opus-bytes-per-sample)))
|
|
(unless (hash-ref h 'sample-format #f)
|
|
(hash-set! h 'sample-format (current-opusfile-output-format)))
|
|
(unless (hash-ref h 'total-samples #f)
|
|
(hash-set! h 'total-samples 0)
|
|
(hash-set! h 'duration 0)))
|
|
|
|
(define (report-format handle)
|
|
(let ((cb (opusfile-handle-cb-info handle)))
|
|
(when (procedure? cb)
|
|
(cb (opusfile-handle-format handle)))))
|
|
|
|
(define (make-format channels total-samples)
|
|
(let ((h (make-hash)))
|
|
(hash-set! h 'duration (if (and (integer? total-samples) (>= total-samples 0))
|
|
(exact->inexact (/ total-samples opus-sample-rate))
|
|
0.0))
|
|
(hash-set! h 'sample-rate opus-sample-rate)
|
|
(hash-set! h 'channels channels)
|
|
(hash-set! h 'bits-per-sample (opus-bits-per-sample))
|
|
(hash-set! h 'bytes-per-sample (opus-bytes-per-sample))
|
|
(hash-set! h 'sample-format (current-opusfile-output-format))
|
|
(hash-set! h 'total-samples total-samples)
|
|
h))
|
|
|
|
(define (give-audio handle buffer size)
|
|
(let ((h (opusfile-handle-format handle)))
|
|
(correct-format-hash h)
|
|
(hash-set! h 'sample (opusfile-handle-pcm-pos handle))
|
|
(hash-set! h 'current-time (exact->inexact (/ (opusfile-handle-pcm-pos handle)
|
|
opus-sample-rate)))
|
|
((opusfile-handle-cb-audio handle) h buffer size)))
|
|
|
|
(define s24-pos-scale #x7FFFFF)
|
|
(define s24-neg-scale #x800000)
|
|
|
|
(define (clip-sample x)
|
|
(cond [(< x -1.0) -1.0]
|
|
[(> x 1.0) 1.0]
|
|
[else x]))
|
|
|
|
(define (float->s24 x)
|
|
(let ((y (clip-sample x)))
|
|
(if (negative? y)
|
|
(inexact->exact (round (* y s24-neg-scale)))
|
|
(inexact->exact (round (* y s24-pos-scale))))))
|
|
|
|
(define (write-s24-native! bs offset sample)
|
|
(let ((v (if (negative? sample) (+ sample #x1000000) sample)))
|
|
(if (system-big-endian?)
|
|
(begin
|
|
(bytes-set! bs offset (bitwise-and (arithmetic-shift v -16) #xFF))
|
|
(bytes-set! bs (+ offset 1) (bitwise-and (arithmetic-shift v -8) #xFF))
|
|
(bytes-set! bs (+ offset 2) (bitwise-and v #xFF)))
|
|
(begin
|
|
(bytes-set! bs offset (bitwise-and v #xFF))
|
|
(bytes-set! bs (+ offset 1) (bitwise-and (arithmetic-shift v -8) #xFF))
|
|
(bytes-set! bs (+ offset 2) (bitwise-and (arithmetic-shift v -16) #xFF))))))
|
|
|
|
(define (opusfile-valid? audio-file)
|
|
(and (opusfile-available?)
|
|
(file-exists? audio-file)
|
|
#t))
|
|
|
|
(define (opusfile-open audio-file* cb-stream-info cb-audio)
|
|
(check-libopusfile 'opusfile-open)
|
|
(let ((audio-file (if (path? audio-file*)
|
|
(path->string audio-file*)
|
|
audio-file*)))
|
|
(if (file-exists? audio-file)
|
|
(let-values (((of err) (op_open_file audio-file)))
|
|
(if of
|
|
(let* ((channels (op_channel_count of -1))
|
|
(total-samples (op_pcm_total of -1))
|
|
(fmt (make-format channels total-samples))
|
|
(h (make-opusfile-handle of cb-stream-info cb-audio #f #f #f fmt 0)))
|
|
(report-format h)
|
|
h)
|
|
(raise-opus 'opusfile-open
|
|
"could not open Opus file ~a; opusfile error code: ~a"
|
|
audio-file err)))
|
|
#f)))
|
|
|
|
(define (handle-pending-seek! handle)
|
|
(unless (eq? (opusfile-handle-seek handle) #f)
|
|
(let ((sample (opusfile-handle-seek handle)))
|
|
(dbg-sound "Seeking opusfile to sample ~a" sample)
|
|
(let ((r (op_pcm_seek (opusfile-handle-of handle) sample)))
|
|
(when (negative? r)
|
|
(err-sound "opusfile seek error: ~a" r))
|
|
(when (not (negative? r))
|
|
(set-opusfile-handle-pcm-pos! handle sample)))
|
|
(set-opusfile-handle-seek! handle #f))))
|
|
|
|
(define (read-s16 handle channels)
|
|
(let* ((max-samples (* default-frames-per-read channels))
|
|
(buffer (make-bytes (* max-samples 2))))
|
|
(let-values (((read-frames link-index)
|
|
(op_read (opusfile-handle-of handle) buffer max-samples)))
|
|
(cond [(negative? read-frames)
|
|
(values read-frames #f 0)]
|
|
[(zero? read-frames)
|
|
(values 0 #f 0)]
|
|
[else
|
|
(let* ((read-samples (* read-frames channels))
|
|
(read-bytes (* read-samples 2))
|
|
(out (if (= read-bytes (bytes-length buffer)) buffer (subbytes buffer 0 read-bytes))))
|
|
(values read-frames out read-bytes))]))))
|
|
|
|
(define (read-s24 handle channels)
|
|
(let* ((max-samples (* default-frames-per-read channels))
|
|
(float-buffer (malloc _float max-samples 'atomic-interior)))
|
|
(let-values (((read-frames link-index)
|
|
(op_read_float (opusfile-handle-of handle) float-buffer max-samples)))
|
|
(cond [(negative? read-frames)
|
|
(values read-frames #f 0)]
|
|
[(zero? read-frames)
|
|
(values 0 #f 0)]
|
|
[else
|
|
(let* ((read-samples (* read-frames channels))
|
|
(out (make-bytes (* read-samples 3))))
|
|
(for ([i (in-range read-samples)])
|
|
(write-s24-native! out (* i 3) (float->s24 (ptr-ref float-buffer _float i))))
|
|
(values read-frames out (bytes-length out)))]))))
|
|
|
|
(define (read-audio-buffer handle channels)
|
|
(case (current-opusfile-output-format)
|
|
[(s16) (read-s16 handle channels)]
|
|
[(s24) (read-s24 handle channels)]))
|
|
|
|
(define (opusfile-read handle)
|
|
(set-opusfile-handle-stop! handle #f)
|
|
(set-opusfile-handle-reading! handle #t)
|
|
(let loop ()
|
|
(cond
|
|
[(opusfile-handle-stop handle)
|
|
(dbg-sound "Stopping opusfile decoding")
|
|
(set-opusfile-handle-reading! handle #f)
|
|
'stopped-reading]
|
|
[else
|
|
(handle-pending-seek! handle)
|
|
(let ((channels (hash-ref (opusfile-handle-format handle) 'channels 2)))
|
|
(let-values (((read-frames out read-bytes) (read-audio-buffer handle channels)))
|
|
(cond [(negative? read-frames)
|
|
(err-sound "opusfile decode error: ~a" read-frames)
|
|
(set-opusfile-handle-stop! handle #t)
|
|
(loop)]
|
|
[(zero? read-frames)
|
|
(set-opusfile-handle-stop! handle #t)
|
|
(loop)]
|
|
[else
|
|
(give-audio handle out read-bytes)
|
|
(set-opusfile-handle-pcm-pos! handle (+ (opusfile-handle-pcm-pos handle) read-frames))
|
|
(loop)])))]))
|
|
(op_free (opusfile-handle-of handle))
|
|
(set-opusfile-handle-reading! handle #f))
|
|
|
|
(define (opusfile-seek handle percentage)
|
|
(let* ((fmt (opusfile-handle-format handle))
|
|
(total-samples (hash-ref fmt 'total-samples 0)))
|
|
(unless (or (eq? total-samples #f) (= total-samples -1) (= total-samples 0))
|
|
(let* ((percentage (max 0 (min 100 percentage)))
|
|
(sample (inexact->exact
|
|
(round (* (exact->inexact (/ percentage 100.0))
|
|
total-samples)))))
|
|
(set-opusfile-handle-seek! handle sample)))))
|
|
|
|
(define (opusfile-stop handle)
|
|
(set-opusfile-handle-stop! handle #t)
|
|
(while (opusfile-handle-reading handle)
|
|
(sleep 0.01)))
|
|
|
|
) ; end of module
|