soxr
This commit is contained in:
@@ -14,8 +14,7 @@ backends:
|
||||
- libao
|
||||
- libFLAC
|
||||
- mpg123
|
||||
- FFmpeg libraries, including libavutil, libavcodec, libavformat and
|
||||
libswresample
|
||||
- FFmpeg libraries, including libavutil, libavcodec and libavformat
|
||||
|
||||
For encoding, also install:
|
||||
|
||||
@@ -23,10 +22,10 @@ For encoding, also install:
|
||||
- libopus
|
||||
- libogg
|
||||
- TagLib with the C binding, usually provided as `taglib` / `taglib_c`
|
||||
- libsoxr, used by `resampler.rkt` and encoder-side PCM sample-rate conversion
|
||||
|
||||
The Opus encoder backend uses libopusenc directly. The FLAC encoder backend
|
||||
uses libFLAC directly. FLAC sample-rate conversion uses the existing FFmpeg
|
||||
swresample layer. Metadata and cover-art copying use the TagLib wrapper; the
|
||||
uses libFLAC directly. FLAC sample-rate conversion uses `resampler.rkt`, backed by libsoxr. Metadata and cover-art copying use the TagLib wrapper; the
|
||||
public `taglib.rkt` API also supports read-write tag editing.
|
||||
|
||||
|
||||
@@ -43,7 +42,7 @@ sudo apt install \
|
||||
ffmpeg \
|
||||
libavutil-dev libavcodec-dev libavformat-dev libswresample-dev \
|
||||
libogg-dev libopus-dev libopusenc-dev libopusfile-dev \
|
||||
libtag1-dev
|
||||
libsoxr-dev libtag1-dev
|
||||
```
|
||||
|
||||
For Opus encoding, `libopusenc-dev` is required. For Opus decoding through
|
||||
@@ -60,6 +59,7 @@ brew install mpg123
|
||||
brew install ffmpeg
|
||||
brew install opus
|
||||
brew install libopusenc
|
||||
brew install libsoxr
|
||||
brew install taglib
|
||||
```
|
||||
|
||||
|
||||
@@ -68,7 +68,8 @@
|
||||
[else "libao"]))
|
||||
(linux-lib-versions '("4" #f))))
|
||||
|
||||
(define-ffi-definer define-ao libao)
|
||||
(define-ffi-definer define-ao libao
|
||||
#:default-make-fail make-not-available)
|
||||
|
||||
(define _ao-device (_cpointer/null 'ao-device))
|
||||
(define _ao-option (_cpointer/null 'ao-option))
|
||||
|
||||
+21
-58
@@ -2,7 +2,7 @@
|
||||
|
||||
(require ffi/unsafe
|
||||
"utils.rkt"
|
||||
"../ffmpeg-definitions.rkt")
|
||||
"../resampler.rkt")
|
||||
|
||||
(provide pcm-conversion-needed?
|
||||
make-pcm-converter
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
(define S32-BYTES 4)
|
||||
|
||||
(define-struct pcm-converter (swr-ctx in-layout out-layout input-format output-format channels in-rate out-rate closed?)
|
||||
(define-struct pcm-converter (resampler input-format output-format channels in-rate out-rate closed?)
|
||||
#:mutable
|
||||
#:constructor-name make-raw-pcm-converter)
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
(int-bytes->integer bs #t (system-big-endian?) start (+ start bytes)))
|
||||
|
||||
(define (native-signed-set! bs start bytes value)
|
||||
(integer->integer-bytes value bytes #t (system-big-endian?) bs start))
|
||||
(integer->int-bytes value bytes #t (system-big-endian?) bs start))
|
||||
|
||||
(define (clamp-s32 v)
|
||||
(cond [(< v -2147483648) -2147483648]
|
||||
@@ -55,21 +55,6 @@
|
||||
(native-signed-set! out out-off S32-BYTES (expand-sample-to-s32 sample in-bits))))
|
||||
out)]))
|
||||
|
||||
(define (bytes->native bs size)
|
||||
(let ((p (malloc size 'atomic-interior)))
|
||||
(memcpy p 0 bs 0 size)
|
||||
p))
|
||||
|
||||
(define (native->bytes p size)
|
||||
(let ((bs (make-bytes size)))
|
||||
(memcpy bs 0 p 0 size)
|
||||
bs))
|
||||
|
||||
(define (make-plane ptr)
|
||||
(let ((planes (malloc _pointer 1 'atomic-interior)))
|
||||
(ptr-set! planes _pointer 0 ptr)
|
||||
planes))
|
||||
|
||||
(define (source-value v source)
|
||||
(if (eq? v 'source) source v))
|
||||
|
||||
@@ -123,36 +108,26 @@
|
||||
(channels-out (target-channels settings input-format))
|
||||
(rate-in (hash-ref input-format 'sample-rate))
|
||||
(rate-out (target-sample-rate settings input-format))
|
||||
(in-layout (ffmpeg-make-default-channel-layout channels-in))
|
||||
(out-layout (ffmpeg-make-default-channel-layout channels-out)))
|
||||
(let-values (((ret ctx) (swr_alloc_set_opts2 #f
|
||||
out-layout AV_SAMPLE_FMT_S32 rate-out
|
||||
in-layout AV_SAMPLE_FMT_S32 rate-in
|
||||
0 #f)))
|
||||
(when (< ret 0) (error 'make-pcm-converter "swr_alloc_set_opts2 failed: ~a" ret))
|
||||
(let ((ret-init (swr_init ctx)))
|
||||
(when (< ret-init 0) (error 'make-pcm-converter "swr_init failed: ~a" ret-init))
|
||||
(make-raw-pcm-converter ctx in-layout out-layout input-format (make-output-format input-format settings)
|
||||
channels-out rate-in rate-out #f)))))
|
||||
(quality (hash-ref/default settings 'resampler-quality 'hq))
|
||||
(phase (hash-ref/default settings 'resampler-phase 'linear))
|
||||
(steep? (hash-ref/default settings 'resampler-steep-filter? #f))
|
||||
(out-format (make-output-format input-format settings)))
|
||||
(unless (= channels-in channels-out)
|
||||
(error 'make-pcm-converter
|
||||
"SoXR PCM converter only supports unchanged channel count; got input ~a and output ~a"
|
||||
channels-in channels-out))
|
||||
(let ((r (make-resampler rate-in rate-out channels-in
|
||||
#:input-format 's32
|
||||
#:output-format 's32
|
||||
#:quality quality
|
||||
#:phase phase
|
||||
#:steep-filter? steep?)))
|
||||
(make-raw-pcm-converter r input-format out-format channels-out rate-in rate-out #f))))
|
||||
|
||||
(define (ensure-open! c who)
|
||||
(when (or (not (pcm-converter? c)) (pcm-converter-closed? c))
|
||||
(error who "PCM converter is closed")))
|
||||
|
||||
(define (convert* c in-bytes in-samples)
|
||||
(let* ((channels (pcm-converter-channels c))
|
||||
(max-out-samples (swr_get_out_samples (pcm-converter-swr-ctx c) in-samples)))
|
||||
(cond [(<= max-out-samples 0) (values #"" 0)]
|
||||
[else
|
||||
(let* ((out-size (* max-out-samples channels S32-BYTES))
|
||||
(out-ptr (malloc out-size 'atomic-interior))
|
||||
(out-planes (make-plane out-ptr))
|
||||
(in-ptr (and in-bytes (bytes->native in-bytes (bytes-length in-bytes))))
|
||||
(in-planes (and in-ptr (make-plane in-ptr)))
|
||||
(out-samples (swr_convert (pcm-converter-swr-ctx c) out-planes max-out-samples in-planes in-samples)))
|
||||
(when (< out-samples 0) (error 'pcm-converter-convert "swr_convert failed: ~a" out-samples))
|
||||
(values (native->bytes out-ptr (* out-samples channels S32-BYTES)) out-samples))])))
|
||||
|
||||
(define (pcm-converter-convert c buffer size buf-info)
|
||||
(ensure-open! c 'pcm-converter-convert)
|
||||
(let* ((in-bits (hash-ref/default buf-info 'bits-per-sample (hash-ref (pcm-converter-input-format c) 'bits-per-sample)))
|
||||
@@ -160,27 +135,15 @@
|
||||
(in-channels (hash-ref (pcm-converter-input-format c) 'channels))
|
||||
(in-samples (quotient (quotient size in-bytes) in-channels))
|
||||
(s32 (pcm-bytes->s32-bytes buffer size in-bits)))
|
||||
(convert* c s32 in-samples)))
|
||||
(resampler-convert (pcm-converter-resampler c) s32 (* in-samples in-channels S32-BYTES))))
|
||||
|
||||
(define (pcm-converter-drain c)
|
||||
(ensure-open! c 'pcm-converter-drain)
|
||||
(let* ((ctx (pcm-converter-swr-ctx c))
|
||||
(delay (swr_get_delay ctx (pcm-converter-out-rate c)))
|
||||
(channels (pcm-converter-channels c)))
|
||||
(cond [(<= delay 0) (values #"" 0)]
|
||||
[else
|
||||
(let* ((out-size (* delay channels S32-BYTES))
|
||||
(out-ptr (malloc out-size 'atomic-interior))
|
||||
(out-planes (make-plane out-ptr))
|
||||
(out-samples (swr_convert ctx out-planes delay #f 0)))
|
||||
(when (< out-samples 0) (error 'pcm-converter-drain "swr_convert drain failed: ~a" out-samples))
|
||||
(values (native->bytes out-ptr (* out-samples channels S32-BYTES)) out-samples))])))
|
||||
(resampler-drain (pcm-converter-resampler c)))
|
||||
|
||||
(define (pcm-converter-close! c)
|
||||
(when (and (pcm-converter? c) (not (pcm-converter-closed? c)))
|
||||
(set-pcm-converter-swr-ctx! c (swr_free (pcm-converter-swr-ctx c)))
|
||||
(ffmpeg-channel-layout-uninit! (pcm-converter-in-layout c))
|
||||
(ffmpeg-channel-layout-uninit! (pcm-converter-out-layout c))
|
||||
(resampler-close! (pcm-converter-resampler c))
|
||||
(set-pcm-converter-closed?! c #t))
|
||||
#t)
|
||||
|
||||
|
||||
+6
-4
@@ -178,14 +178,14 @@
|
||||
(loop (+ n 1) (cons (number->string n) acc)))))
|
||||
|
||||
(define (ffmpeg-lib-versions kind)
|
||||
(case (system-type 'os)
|
||||
(case (system-type 'os*)
|
||||
[(linux)
|
||||
(let ((v (hash-ref valid-ffmpeg-versions kind)))
|
||||
(versions-high-to-low (car v) (cadr v)))]
|
||||
[else '(#f)]))
|
||||
|
||||
(define (linux-lib-versions versions [default '(#f)])
|
||||
(case (system-type 'os)
|
||||
(case (system-type 'os*)
|
||||
[(linux) versions]
|
||||
[else default]))
|
||||
|
||||
@@ -213,7 +213,7 @@
|
||||
"
|
||||
" libavcodec60 libavutil58 libswresample4 libavformat60 \
|
||||
"
|
||||
" libogg0 libopus0 libopusenc0 libopusfile0 libtag1v5
|
||||
" libogg0 libopus0 libopusenc0 libopusfile0 libsoxr0 libtag1v5
|
||||
"
|
||||
"
|
||||
"
|
||||
@@ -223,7 +223,7 @@
|
||||
"
|
||||
"libavutil-dev, libswresample-dev, libavformat-dev, libogg-dev,
|
||||
"
|
||||
"libopus-dev, libopusenc-dev, libopusfile-dev and libtag1-dev.
|
||||
"libopus-dev, libopusenc-dev, libopusfile-dev, libsoxr-dev and libtag1-dev.
|
||||
"
|
||||
"
|
||||
"
|
||||
@@ -244,6 +244,8 @@
|
||||
" brew install opus
|
||||
"
|
||||
" brew install libopusenc
|
||||
"
|
||||
" brew install libsoxr
|
||||
"
|
||||
" brew install taglib
|
||||
"
|
||||
|
||||
+285
@@ -0,0 +1,285 @@
|
||||
(module resampler racket/base
|
||||
|
||||
(require ffi/unsafe
|
||||
"soxr-ffi.rkt"
|
||||
"private/utils.rkt")
|
||||
|
||||
(provide resampler-available?
|
||||
resampler-version
|
||||
make-resampler
|
||||
resampler?
|
||||
resampler-input-rate
|
||||
resampler-output-rate
|
||||
resampler-channels
|
||||
resampler-input-format
|
||||
resampler-output-format
|
||||
resampler-convert
|
||||
resampler-drain
|
||||
resampler-clear!
|
||||
resampler-close!
|
||||
resample-bytes
|
||||
pcm-format?
|
||||
pcm-format-sample-bytes
|
||||
pcm-format->soxr-datatype
|
||||
quality->soxr-recipe)
|
||||
|
||||
(define-struct resampler
|
||||
(ctx input-rate output-rate channels input-format output-format
|
||||
soxr-input-format soxr-output-format input-sample-bytes output-sample-bytes closed?)
|
||||
#:mutable
|
||||
#:constructor-name make-raw-resampler)
|
||||
|
||||
(define (resampler-available?) (soxr-available?))
|
||||
(define (resampler-version) (soxr-version))
|
||||
|
||||
(define (normalize-format fmt who)
|
||||
(cond [(or (eq? fmt 's16) (eq? fmt 'int16) (eq? fmt 'int16-interleaved)) 's16]
|
||||
[(or (eq? fmt 's24) (eq? fmt 'int24) (eq? fmt 'int24-interleaved)) 's24]
|
||||
[(or (eq? fmt 's32) (eq? fmt 'int32) (eq? fmt 'int32-interleaved)) 's32]
|
||||
[(or (eq? fmt 'float32) (eq? fmt 'f32) (eq? fmt 'float)) 'float32]
|
||||
[(or (eq? fmt 'float64) (eq? fmt 'f64) (eq? fmt 'double)) 'float64]
|
||||
[else (raise-argument-error who "(or/c 's16 's24 's32 'float32 'float64)" fmt)]))
|
||||
|
||||
(define (pcm-format? fmt)
|
||||
(with-handlers ([exn:fail? (lambda (e) #f)])
|
||||
(normalize-format fmt 'pcm-format?)
|
||||
#t))
|
||||
|
||||
(define (pcm-format-sample-bytes fmt)
|
||||
(case (normalize-format fmt 'pcm-format-sample-bytes)
|
||||
[(s16) 2]
|
||||
[(s24) 3]
|
||||
[(s32 float32) 4]
|
||||
[(float64) 8]))
|
||||
|
||||
(define (pcm-format-actual-sample-bytes fmt)
|
||||
;; SoXR has no packed 24-bit datatype. The wrapper expands s24 to s32
|
||||
;; before processing and packs s32 back to s24 afterwards when requested.
|
||||
(case (normalize-format fmt 'pcm-format-actual-sample-bytes)
|
||||
[(s24) 4]
|
||||
[else (pcm-format-sample-bytes fmt)]))
|
||||
|
||||
(define (pcm-format->soxr-datatype fmt)
|
||||
(case (normalize-format fmt 'pcm-format->soxr-datatype)
|
||||
[(s16) SOXR_INT16_I]
|
||||
[(s24 s32) SOXR_INT32_I]
|
||||
[(float32) SOXR_FLOAT32_I]
|
||||
[(float64) SOXR_FLOAT64_I]))
|
||||
|
||||
(define (quality->soxr-recipe q)
|
||||
;; The '*-bitq' recipes are resampling precision recipes; they do not mean
|
||||
;; that the PCM output is 16/24/32-bit. Output format is controlled by
|
||||
;; #:output-format.
|
||||
(cond [(integer? q) q]
|
||||
[(or (eq? q 'qq) (eq? q 'quick)) SOXR_QQ]
|
||||
[(or (eq? q 'lq) (eq? q 'low)) SOXR_LQ]
|
||||
[(or (eq? q 'mq) (eq? q 'medium)) SOXR_MQ]
|
||||
[(or (eq? q 'hq) (eq? q 'high)) SOXR_HQ]
|
||||
[(or (eq? q 'vhq) (eq? q 'very-high)) SOXR_VHQ]
|
||||
[(eq? q '16-bit) SOXR_16_BITQ]
|
||||
[(eq? q '20-bit) SOXR_20_BITQ]
|
||||
[(eq? q '24-bit) SOXR_24_BITQ]
|
||||
[(eq? q '28-bit) SOXR_28_BITQ]
|
||||
[(eq? q '32-bit) SOXR_32_BITQ]
|
||||
[else (raise-argument-error 'quality->soxr-recipe
|
||||
"(or/c 'qq 'lq 'mq 'hq 'vhq '16-bit '20-bit '24-bit '28-bit '32-bit exact-integer?)" q)]))
|
||||
|
||||
(define (phase->flags phase)
|
||||
(cond [(or (eq? phase 'linear) (eq? phase #f)) SOXR_LINEAR_PHASE]
|
||||
[(or (eq? phase 'intermediate) (eq? phase 'medium)) SOXR_INTERMEDIATE_PHASE]
|
||||
[(or (eq? phase 'minimum) (eq? phase 'min)) SOXR_MINIMUM_PHASE]
|
||||
[else (raise-argument-error 'make-resampler "(or/c 'linear 'intermediate 'minimum)" phase)]))
|
||||
|
||||
(define (check-error who err)
|
||||
(when err (error who "~a" err)))
|
||||
|
||||
(define (bytes->native bs size)
|
||||
(let ((p (malloc size 'atomic-interior)))
|
||||
(memcpy p 0 bs 0 size)
|
||||
p))
|
||||
|
||||
(define (native->bytes p size)
|
||||
(let ((bs (make-bytes size)))
|
||||
(memcpy bs 0 p 0 size)
|
||||
bs))
|
||||
|
||||
(define (native-signed-ref bs start bytes)
|
||||
(int-bytes->integer bs #t (system-big-endian?) start (+ start bytes)))
|
||||
|
||||
(define (native-signed-set! bs start bytes value)
|
||||
(integer->int-bytes value bytes #t (system-big-endian?) bs start))
|
||||
|
||||
(define (clamp-s32 v)
|
||||
(cond [(< v -2147483648) -2147483648]
|
||||
[(> v 2147483647) 2147483647]
|
||||
[else v]))
|
||||
|
||||
(define (clamp-s24 v)
|
||||
(cond [(< v -8388608) -8388608]
|
||||
[(> v 8388607) 8388607]
|
||||
[else v]))
|
||||
|
||||
(define (s24-bytes->s32-bytes buffer size)
|
||||
(let* ((sample-count (quotient size 3))
|
||||
(out (make-bytes (* sample-count 4))))
|
||||
(for ([i (in-range sample-count)])
|
||||
(let* ((in-off (* i 3))
|
||||
(out-off (* i 4))
|
||||
(sample (native-signed-ref buffer in-off 3)))
|
||||
(native-signed-set! out out-off 4 (clamp-s32 (arithmetic-shift sample 8)))))
|
||||
out))
|
||||
|
||||
(define (s32-bytes->s24-bytes buffer size)
|
||||
(let* ((sample-count (quotient size 4))
|
||||
(out (make-bytes (* sample-count 3))))
|
||||
(for ([i (in-range sample-count)])
|
||||
(let* ((in-off (* i 4))
|
||||
(out-off (* i 3))
|
||||
(sample (native-signed-ref buffer in-off 4)))
|
||||
(native-signed-set! out out-off 3 (clamp-s24 (arithmetic-shift sample -8)))))
|
||||
out))
|
||||
|
||||
(define (subbytes/size bs size)
|
||||
(if (= size (bytes-length bs)) bs (subbytes bs 0 size)))
|
||||
|
||||
(define (prepare-input-bytes r buffer size)
|
||||
(case (resampler-input-format r)
|
||||
[(s24) (s24-bytes->s32-bytes buffer size)]
|
||||
[else (subbytes/size buffer size)]))
|
||||
|
||||
(define (finish-output-bytes r buffer size)
|
||||
(case (resampler-output-format r)
|
||||
[(s24) (s32-bytes->s24-bytes buffer size)]
|
||||
[else (subbytes/size buffer size)]))
|
||||
|
||||
(define (output-frame-capacity r in-frames)
|
||||
(let* ((ratio (/ (resampler-output-rate r) (resampler-input-rate r)))
|
||||
(delay (if (resampler-ctx r) (max 0.0 (soxr-delay (resampler-ctx r))) 0.0))
|
||||
(n (ceiling (+ 256 delay (* in-frames ratio)))))
|
||||
(max 256 (inexact->exact n))))
|
||||
|
||||
(define (make-resampler input-rate output-rate channels
|
||||
#:input-format [input-format 's32]
|
||||
#:output-format [output-format 's32]
|
||||
#:quality [quality 'hq]
|
||||
#:phase [phase 'linear]
|
||||
#:steep-filter? [steep-filter? #f]
|
||||
#:scale [scale 1.0]
|
||||
#:no-dither? [no-dither? #f]
|
||||
#:num-threads [num-threads 1])
|
||||
(unless (resampler-available?)
|
||||
(error 'make-resampler "libsoxr is not available"))
|
||||
(unless (and (integer? channels) (> channels 0))
|
||||
(raise-argument-error 'make-resampler "exact-positive-integer?" channels))
|
||||
(let* ((in-fmt (normalize-format input-format 'make-resampler))
|
||||
(out-fmt (normalize-format output-format 'make-resampler))
|
||||
(itype (pcm-format->soxr-datatype in-fmt))
|
||||
(otype (pcm-format->soxr-datatype out-fmt))
|
||||
(io-flags (if no-dither? SOXR_NO_DITHER 0))
|
||||
(quality-flags (bitwise-ior (phase->flags phase) (if steep-filter? SOXR_STEEP_FILTER 0)))
|
||||
(io-spec (soxr-io-spec itype otype #:scale scale #:flags io-flags))
|
||||
(quality-spec (soxr-quality-spec (quality->soxr-recipe quality) #:flags quality-flags))
|
||||
(runtime-spec (soxr-runtime-spec #:num-threads num-threads)))
|
||||
(let-values (((ctx err) (soxr-create input-rate output-rate channels
|
||||
#:io-spec io-spec
|
||||
#:quality-spec quality-spec
|
||||
#:runtime-spec runtime-spec)))
|
||||
(check-error 'make-resampler err)
|
||||
(unless ctx (error 'make-resampler "libsoxr returned no resampler context"))
|
||||
(make-raw-resampler ctx input-rate output-rate channels in-fmt out-fmt itype otype
|
||||
(pcm-format-actual-sample-bytes in-fmt)
|
||||
(pcm-format-actual-sample-bytes out-fmt)
|
||||
#f))))
|
||||
|
||||
(define (ensure-open! r who)
|
||||
(when (or (not (resampler? r)) (resampler-closed? r))
|
||||
(error who "resampler is closed")))
|
||||
|
||||
(define (check-frame-size who size frame-bytes)
|
||||
(unless (= (remainder size frame-bytes) 0)
|
||||
(error who "buffer size ~a is not a whole number of interleaved frames of ~a bytes" size frame-bytes)))
|
||||
|
||||
(define (resampler-convert r buffer [size (bytes-length buffer)])
|
||||
(ensure-open! r 'resampler-convert)
|
||||
(let* ((nominal-in-frame-bytes (* (resampler-channels r) (pcm-format-sample-bytes (resampler-input-format r))))
|
||||
(actual-in-frame-bytes (* (resampler-channels r) (resampler-input-sample-bytes r))))
|
||||
(check-frame-size 'resampler-convert size nominal-in-frame-bytes)
|
||||
(cond [(zero? size) (values #"" 0)]
|
||||
[else
|
||||
(let* ((in-frames (quotient size nominal-in-frame-bytes))
|
||||
(prepared (prepare-input-bytes r buffer size))
|
||||
(in-ptr0 (bytes->native prepared (bytes-length prepared))))
|
||||
(let loop ((offset-frames 0) (remaining in-frames) (pieces '()) (total-frames 0))
|
||||
(cond [(zero? remaining) (values (apply bytes-append (reverse pieces)) total-frames)]
|
||||
[else
|
||||
(let* ((out-cap (output-frame-capacity r remaining))
|
||||
(out-size (* out-cap (resampler-channels r) (resampler-output-sample-bytes r)))
|
||||
(out-ptr (malloc out-size 'atomic-interior))
|
||||
(in-ptr (ptr-add in-ptr0 (* offset-frames actual-in-frame-bytes))))
|
||||
(let-values (((err idone odone) (soxr-process (resampler-ctx r) in-ptr remaining out-ptr out-cap)))
|
||||
(check-error 'resampler-convert err)
|
||||
(when (and (> remaining 0) (zero? idone) (zero? odone))
|
||||
(error 'resampler-convert "libsoxr made no progress"))
|
||||
(let* ((raw-size (* odone (resampler-channels r) (resampler-output-sample-bytes r)))
|
||||
(raw (native->bytes out-ptr raw-size))
|
||||
(piece (finish-output-bytes r raw raw-size)))
|
||||
(loop (+ offset-frames idone) (- remaining idone)
|
||||
(if (zero? odone) pieces (cons piece pieces))
|
||||
(+ total-frames odone)))))])))])))
|
||||
|
||||
(define (resampler-drain r)
|
||||
(ensure-open! r 'resampler-drain)
|
||||
(let loop ((pieces '()) (total-frames 0))
|
||||
(let* ((delay (max 0.0 (soxr-delay (resampler-ctx r))))
|
||||
(out-cap (max 4096 (inexact->exact (ceiling (+ delay 256)))))
|
||||
(out-size (* out-cap (resampler-channels r) (resampler-output-sample-bytes r)))
|
||||
(out-ptr (malloc out-size 'atomic-interior)))
|
||||
(let-values (((err idone odone) (soxr-process (resampler-ctx r) #f 0 out-ptr out-cap)))
|
||||
(check-error 'resampler-drain err)
|
||||
(cond [(zero? odone) (values (apply bytes-append (reverse pieces)) total-frames)]
|
||||
[else
|
||||
(let* ((raw-size (* odone (resampler-channels r) (resampler-output-sample-bytes r)))
|
||||
(raw (native->bytes out-ptr raw-size))
|
||||
(piece (finish-output-bytes r raw raw-size)))
|
||||
(loop (cons piece pieces) (+ total-frames odone)))])))))
|
||||
|
||||
(define (resampler-clear! r)
|
||||
(ensure-open! r 'resampler-clear!)
|
||||
(check-error 'resampler-clear! (soxr-clear (resampler-ctx r)))
|
||||
#t)
|
||||
|
||||
(define (resampler-close! r)
|
||||
(when (and (resampler? r) (not (resampler-closed? r)))
|
||||
(soxr-delete (resampler-ctx r))
|
||||
(set-resampler-ctx! r #f)
|
||||
(set-resampler-closed?! r #t))
|
||||
#t)
|
||||
|
||||
(define (resample-bytes buffer input-rate output-rate channels
|
||||
#:size [size (bytes-length buffer)]
|
||||
#:input-format [input-format 's32]
|
||||
#:output-format [output-format 's32]
|
||||
#:quality [quality 'hq]
|
||||
#:phase [phase 'linear]
|
||||
#:steep-filter? [steep-filter? #f]
|
||||
#:scale [scale 1.0]
|
||||
#:no-dither? [no-dither? #f]
|
||||
#:num-threads [num-threads 1])
|
||||
(let ((r (make-resampler input-rate output-rate channels
|
||||
#:input-format input-format
|
||||
#:output-format output-format
|
||||
#:quality quality
|
||||
#:phase phase
|
||||
#:steep-filter? steep-filter?
|
||||
#:scale scale
|
||||
#:no-dither? no-dither?
|
||||
#:num-threads num-threads)))
|
||||
(dynamic-wind
|
||||
void
|
||||
(lambda ()
|
||||
(let-values (((a af) (resampler-convert r buffer size)))
|
||||
(let-values (((b bf) (resampler-drain r)))
|
||||
(values (bytes-append a b) (+ af bf)))))
|
||||
(lambda () (resampler-close! r)))))
|
||||
|
||||
) ; end of module
|
||||
@@ -166,9 +166,10 @@ The first backend version supports mono and stereo input.
|
||||
|
||||
The FLAC backend uses the @tt{libFLAC} stream encoder. It writes interleaved
|
||||
integer PCM samples through the FLAC encoder API. When the requested output
|
||||
format differs from the decoded input format, @racketmodname[racket-audio/private/pcm-converter]
|
||||
uses the existing FFmpeg @tt{swresample} layer from
|
||||
@racketmodname[racket-audio/ffmpeg-definitions] to perform PCM normalisation.
|
||||
sample rate differs from the decoded input format, @racketmodname[racket-audio/private/pcm-converter]
|
||||
uses @racketmodname[racket-audio/resampler], backed by @tt{libsoxr}, to perform
|
||||
PCM sample-rate conversion. Channel-count conversion is not handled by this
|
||||
SoXR path; keep the source channel count for encoder-side conversion.
|
||||
|
||||
The following settings are recognised:
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
@include-section["audio-sniffer.scrbl"]
|
||||
@include-section["taglib.scrbl"]
|
||||
@include-section["audio-encoder.scrbl"]
|
||||
@include-section["resampler.scrbl"]
|
||||
@include-section["encoder-test.scrbl"]
|
||||
@include-section["play-test.scrbl"]
|
||||
@include-section["audio-placed-player.scrbl"]
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
#lang scribble/manual
|
||||
|
||||
@title{PCM resampling with SoXR}
|
||||
|
||||
@defmodule[racket-audio/resampler]
|
||||
|
||||
This module provides a small Racket wrapper around @tt{libsoxr}. It is meant
|
||||
for interleaved PCM buffers such as stereo @tt{L R L R ...} data. It supports
|
||||
sample-rate conversion and the PCM datatypes that SoXR can process directly:
|
||||
@racket['s16], @racket['s32], @racket['float32] and @racket['float64]. Packed
|
||||
24-bit PCM is supported by the wrapper by expanding @racket['s24] input to
|
||||
@racket['s32] before calling SoXR and packing @racket['s32] output back to
|
||||
@racket['s24] afterwards.
|
||||
|
||||
SoXR does not perform general channel-layout conversion in this wrapper. Use it
|
||||
for unchanged channel counts, for example mono-to-mono or stereo-to-stereo.
|
||||
|
||||
@defproc[(resampler-available?) boolean?]{
|
||||
Returns @racket[#t] when @tt{libsoxr} was loaded.
|
||||
}
|
||||
|
||||
@defproc[(resampler-version) string?]{
|
||||
Returns the version string reported by @tt{libsoxr}.
|
||||
}
|
||||
|
||||
@defproc[(make-resampler
|
||||
[input-rate exact-positive-integer?]
|
||||
[output-rate exact-positive-integer?]
|
||||
[channels exact-positive-integer?]
|
||||
[#:input-format input-format symbol? 's32]
|
||||
[#:output-format output-format symbol? 's32]
|
||||
[#:quality quality symbol? 'hq]
|
||||
[#:phase phase symbol? 'linear]
|
||||
[#:steep-filter? steep-filter? boolean? #f]
|
||||
[#:scale scale real? 1.0]
|
||||
[#:no-dither? no-dither? boolean? #f]
|
||||
[#:num-threads num-threads exact-positive-integer? 1])
|
||||
resampler?]{
|
||||
Creates a streaming resampler. The supported PCM formats are @racket['s16],
|
||||
@racket['s24], @racket['s32], @racket['float32] and @racket['float64].
|
||||
|
||||
The quality value selects a SoXR resampling recipe. Useful values are
|
||||
@racket['qq], @racket['lq], @racket['mq], @racket['hq], @racket['vhq],
|
||||
@racket['16-bit], @racket['20-bit], @racket['24-bit], @racket['28-bit] and
|
||||
@racket['32-bit]. The @racket['24-bit] quality recipe is a filter precision
|
||||
setting; it is not the same thing as 24-bit PCM output. Use
|
||||
@racket[#:output-format 's24] for packed 24-bit output.
|
||||
}
|
||||
|
||||
@defproc[(resampler-convert
|
||||
[r resampler?]
|
||||
[buffer bytes?]
|
||||
[size exact-nonnegative-integer? (bytes-length buffer)])
|
||||
(values bytes? exact-nonnegative-integer?)]{
|
||||
Feeds interleaved PCM bytes to the resampler and returns converted PCM bytes and
|
||||
the number of output frames. A frame contains one sample for each channel.
|
||||
}
|
||||
|
||||
@defproc[(resampler-drain [r resampler?])
|
||||
(values bytes? exact-nonnegative-integer?)]{
|
||||
Flushes delayed output after the last input block and returns converted bytes and
|
||||
frames.
|
||||
}
|
||||
|
||||
@defproc[(resampler-clear! [r resampler?]) boolean?]{
|
||||
Clears the resampler state so the same instance can be reused for a fresh signal
|
||||
with the same configuration.
|
||||
}
|
||||
|
||||
@defproc[(resampler-close! [r resampler?]) boolean?]{
|
||||
Releases the native SoXR resampler. Calling this more than once is harmless.
|
||||
}
|
||||
|
||||
@defproc[(resample-bytes
|
||||
[buffer bytes?]
|
||||
[input-rate exact-positive-integer?]
|
||||
[output-rate exact-positive-integer?]
|
||||
[channels exact-positive-integer?]
|
||||
[#:size size exact-nonnegative-integer? (bytes-length buffer)]
|
||||
[#:input-format input-format symbol? 's32]
|
||||
[#:output-format output-format symbol? 's32]
|
||||
[#:quality quality symbol? 'hq]
|
||||
[#:phase phase symbol? 'linear]
|
||||
[#:steep-filter? steep-filter? boolean? #f]
|
||||
[#:scale scale real? 1.0]
|
||||
[#:no-dither? no-dither? boolean? #f]
|
||||
[#:num-threads num-threads exact-positive-integer? 1])
|
||||
(values bytes? exact-nonnegative-integer?)]{
|
||||
Convenience function for one buffer. It creates a resampler, processes the
|
||||
input, drains delayed samples and closes the native state.
|
||||
}
|
||||
|
||||
@defproc[(pcm-format? [v any/c]) boolean?]{
|
||||
Returns whether @racket[v] is one of the supported PCM format symbols.
|
||||
}
|
||||
|
||||
@defproc[(pcm-format-sample-bytes [fmt symbol?]) exact-positive-integer?]{
|
||||
Returns the packed sample size in bytes for @racket[fmt]. For @racket['s24]
|
||||
this is @racket[3], even though the wrapper internally expands to 32-bit samples
|
||||
for SoXR.
|
||||
}
|
||||
+255
@@ -0,0 +1,255 @@
|
||||
(module soxr-ffi racket/base
|
||||
|
||||
(require ffi/unsafe
|
||||
ffi/unsafe/define
|
||||
"private/utils.rkt")
|
||||
|
||||
(provide soxr-available?
|
||||
soxr-version
|
||||
soxr-create
|
||||
soxr-process
|
||||
soxr-delay
|
||||
soxr-clear
|
||||
soxr-delete
|
||||
soxr-error
|
||||
soxr-engine
|
||||
soxr-io-spec
|
||||
soxr-quality-spec
|
||||
soxr-runtime-spec
|
||||
soxr-error-ptr->string
|
||||
soxr-datatype-size
|
||||
|
||||
_soxr_t
|
||||
_soxr_io_spec
|
||||
_soxr_io_spec-pointer
|
||||
soxr_io_spec?
|
||||
soxr_io_spec-itype
|
||||
soxr_io_spec-otype
|
||||
soxr_io_spec-scale
|
||||
soxr_io_spec-flags
|
||||
set-soxr_io_spec-scale!
|
||||
set-soxr_io_spec-flags!
|
||||
|
||||
_soxr_quality_spec
|
||||
_soxr_quality_spec-pointer
|
||||
soxr_quality_spec?
|
||||
soxr_quality_spec-precision
|
||||
soxr_quality_spec-phase_response
|
||||
soxr_quality_spec-passband_end
|
||||
soxr_quality_spec-stopband_begin
|
||||
soxr_quality_spec-flags
|
||||
set-soxr_quality_spec-precision!
|
||||
set-soxr_quality_spec-phase_response!
|
||||
set-soxr_quality_spec-passband_end!
|
||||
set-soxr_quality_spec-stopband_begin!
|
||||
set-soxr_quality_spec-flags!
|
||||
|
||||
_soxr_runtime_spec
|
||||
_soxr_runtime_spec-pointer
|
||||
soxr_runtime_spec?
|
||||
soxr_runtime_spec-log2_min_dft_size
|
||||
soxr_runtime_spec-log2_large_dft_size
|
||||
soxr_runtime_spec-coef_size_kbytes
|
||||
soxr_runtime_spec-num_threads
|
||||
soxr_runtime_spec-flags
|
||||
set-soxr_runtime_spec-num_threads!
|
||||
set-soxr_runtime_spec-flags!
|
||||
|
||||
SOXR_FLOAT32
|
||||
SOXR_FLOAT64
|
||||
SOXR_INT32
|
||||
SOXR_INT16
|
||||
SOXR_SPLIT
|
||||
SOXR_FLOAT32_I
|
||||
SOXR_FLOAT64_I
|
||||
SOXR_INT32_I
|
||||
SOXR_INT16_I
|
||||
SOXR_FLOAT32_S
|
||||
SOXR_FLOAT64_S
|
||||
SOXR_INT32_S
|
||||
SOXR_INT16_S
|
||||
|
||||
SOXR_TPDF
|
||||
SOXR_NO_DITHER
|
||||
SOXR_QQ
|
||||
SOXR_LQ
|
||||
SOXR_MQ
|
||||
SOXR_HQ
|
||||
SOXR_VHQ
|
||||
SOXR_16_BITQ
|
||||
SOXR_20_BITQ
|
||||
SOXR_24_BITQ
|
||||
SOXR_28_BITQ
|
||||
SOXR_32_BITQ
|
||||
SOXR_LINEAR_PHASE
|
||||
SOXR_INTERMEDIATE_PHASE
|
||||
SOXR_MINIMUM_PHASE
|
||||
SOXR_STEEP_FILTER
|
||||
SOXR_ROLLOFF_SMALL
|
||||
SOXR_ROLLOFF_MEDIUM
|
||||
SOXR_ROLLOFF_NONE
|
||||
SOXR_HI_PREC_CLOCK
|
||||
SOXR_DOUBLE_PRECISION
|
||||
SOXR_VR
|
||||
SOXR_COEF_INTERP_AUTO
|
||||
SOXR_COEF_INTERP_LOW
|
||||
SOXR_COEF_INTERP_HIGH)
|
||||
|
||||
;; libsoxr is normally libsoxr.so.0 on Linux and libsoxr.dylib on macOS.
|
||||
;; The Windows build supplied with racket-audio may be either soxr.dll or
|
||||
;; libsoxr.dll, so both stems are tried through the existing get-lib helper.
|
||||
(define libsoxr
|
||||
(get-lib (case (system-type 'os)
|
||||
[(windows) '("soxr" "libsoxr")]
|
||||
[else '("soxr" "libsoxr")])
|
||||
(case (system-type 'os*)
|
||||
[(linux) '("0" #f)]
|
||||
[else '(#f)])))
|
||||
|
||||
(define-ffi-definer def-soxr libsoxr #:default-make-fail make-not-available)
|
||||
|
||||
(define _soxr_t (_cpointer/null 'soxr))
|
||||
|
||||
;; soxr.h structs. These are passed by pointer to soxr_create, while the
|
||||
;; constructor functions in the C API return initialized structs by value.
|
||||
(define-cstruct _soxr_io_spec
|
||||
([itype _int]
|
||||
[otype _int]
|
||||
[scale _double]
|
||||
[e _pointer]
|
||||
[flags _ulong]))
|
||||
|
||||
(define-cstruct _soxr_quality_spec
|
||||
([precision _double]
|
||||
[phase_response _double]
|
||||
[passband_end _double]
|
||||
[stopband_begin _double]
|
||||
[e _pointer]
|
||||
[flags _ulong]))
|
||||
|
||||
(define-cstruct _soxr_runtime_spec
|
||||
([log2_min_dft_size _uint]
|
||||
[log2_large_dft_size _uint]
|
||||
[coef_size_kbytes _uint]
|
||||
[num_threads _uint]
|
||||
[e _pointer]
|
||||
[flags _ulong]))
|
||||
|
||||
;; Datatypes supported by libsoxr. The *_I values are channel-interleaved;
|
||||
;; those are what resampler.rkt uses for normal PCM byte strings.
|
||||
(define SOXR_FLOAT32 0)
|
||||
(define SOXR_FLOAT64 1)
|
||||
(define SOXR_INT32 2)
|
||||
(define SOXR_INT16 3)
|
||||
(define SOXR_SPLIT 4)
|
||||
(define SOXR_FLOAT32_I SOXR_FLOAT32)
|
||||
(define SOXR_FLOAT64_I SOXR_FLOAT64)
|
||||
(define SOXR_INT32_I SOXR_INT32)
|
||||
(define SOXR_INT16_I SOXR_INT16)
|
||||
(define SOXR_FLOAT32_S SOXR_SPLIT)
|
||||
(define SOXR_FLOAT64_S 5)
|
||||
(define SOXR_INT32_S 6)
|
||||
(define SOXR_INT16_S 7)
|
||||
|
||||
(define SOXR_TPDF 0)
|
||||
(define SOXR_NO_DITHER 8)
|
||||
|
||||
(define SOXR_QQ 0)
|
||||
(define SOXR_LQ 1)
|
||||
(define SOXR_MQ 2)
|
||||
(define SOXR_16_BITQ 3)
|
||||
(define SOXR_20_BITQ 4)
|
||||
(define SOXR_24_BITQ 5)
|
||||
(define SOXR_28_BITQ 6)
|
||||
(define SOXR_32_BITQ 7)
|
||||
(define SOXR_HQ SOXR_20_BITQ)
|
||||
(define SOXR_VHQ SOXR_28_BITQ)
|
||||
|
||||
(define SOXR_LINEAR_PHASE #x00)
|
||||
(define SOXR_INTERMEDIATE_PHASE #x10)
|
||||
(define SOXR_MINIMUM_PHASE #x30)
|
||||
(define SOXR_STEEP_FILTER #x40)
|
||||
|
||||
(define SOXR_ROLLOFF_SMALL 0)
|
||||
(define SOXR_ROLLOFF_MEDIUM 1)
|
||||
(define SOXR_ROLLOFF_NONE 2)
|
||||
(define SOXR_HI_PREC_CLOCK 8)
|
||||
(define SOXR_DOUBLE_PRECISION 16)
|
||||
(define SOXR_VR 32)
|
||||
|
||||
(define SOXR_COEF_INTERP_AUTO 0)
|
||||
(define SOXR_COEF_INTERP_LOW 2)
|
||||
(define SOXR_COEF_INTERP_HIGH 3)
|
||||
|
||||
(define (soxr-datatype-size t)
|
||||
(case (bitwise-and t 3)
|
||||
[(0) 4]
|
||||
[(1) 8]
|
||||
[(2) 4]
|
||||
[(3) 2]
|
||||
[else (error 'soxr-datatype-size "invalid datatype ~a" t)]))
|
||||
|
||||
(define (soxr-error-ptr->string p)
|
||||
(and p (cast p _pointer _string/utf-8)))
|
||||
|
||||
(def-soxr soxr_version/raw (_fun -> _string/utf-8) #:c-id soxr_version)
|
||||
(def-soxr soxr_io_spec/raw (_fun _int _int -> _soxr_io_spec) #:c-id soxr_io_spec)
|
||||
(def-soxr soxr_quality_spec/raw (_fun _ulong _ulong -> _soxr_quality_spec) #:c-id soxr_quality_spec)
|
||||
(def-soxr soxr_runtime_spec/raw (_fun _uint -> _soxr_runtime_spec) #:c-id soxr_runtime_spec)
|
||||
|
||||
(def-soxr soxr_create/raw
|
||||
(_fun _double _double _uint
|
||||
(errp : (_ptr o _pointer))
|
||||
_soxr_io_spec-pointer/null
|
||||
_soxr_quality_spec-pointer/null
|
||||
_soxr_runtime_spec-pointer/null
|
||||
-> (ctx : _soxr_t)
|
||||
-> (values ctx errp))
|
||||
#:c-id soxr_create)
|
||||
|
||||
(def-soxr soxr_process/raw
|
||||
(_fun _soxr_t
|
||||
_pointer _size (idone : (_ptr o _size))
|
||||
_pointer _size (odone : (_ptr o _size))
|
||||
-> (err : _pointer)
|
||||
-> (values err idone odone))
|
||||
#:c-id soxr_process)
|
||||
|
||||
(def-soxr soxr-delay (_fun _soxr_t -> _double) #:c-id soxr_delay)
|
||||
(def-soxr soxr-clear (_fun _soxr_t -> (err : _pointer) -> (soxr-error-ptr->string err)) #:c-id soxr_clear)
|
||||
(def-soxr soxr-error (_fun _soxr_t -> (err : _pointer) -> (soxr-error-ptr->string err)) #:c-id soxr_error)
|
||||
(def-soxr soxr-engine (_fun _soxr_t -> _string/utf-8) #:c-id soxr_engine)
|
||||
(def-soxr soxr-delete (_fun _soxr_t -> _void) #:c-id soxr_delete)
|
||||
|
||||
(define (soxr-available?) (and libsoxr #t))
|
||||
|
||||
(define (soxr-version)
|
||||
(soxr_version/raw))
|
||||
|
||||
(define (soxr-io-spec itype otype #:scale [scale 1.0] #:flags [flags 0])
|
||||
(let ((s (soxr_io_spec/raw itype otype)))
|
||||
(set-soxr_io_spec-scale! s scale)
|
||||
(set-soxr_io_spec-flags! s flags)
|
||||
s))
|
||||
|
||||
(define (soxr-quality-spec recipe #:flags [flags 0])
|
||||
(soxr_quality_spec/raw recipe flags))
|
||||
|
||||
(define (soxr-runtime-spec #:num-threads [num-threads 1])
|
||||
(soxr_runtime_spec/raw num-threads))
|
||||
|
||||
(define (soxr-create input-rate output-rate channels #:io-spec [io-spec #f]
|
||||
#:quality-spec [quality-spec #f] #:runtime-spec [runtime-spec #f])
|
||||
(let-values (((ctx err) (soxr_create/raw (exact->inexact input-rate)
|
||||
(exact->inexact output-rate)
|
||||
channels
|
||||
io-spec
|
||||
quality-spec
|
||||
runtime-spec)))
|
||||
(values ctx (soxr-error-ptr->string err))))
|
||||
|
||||
(define (soxr-process ctx in ilen out olen)
|
||||
(let-values (((err idone odone) (soxr_process/raw ctx in ilen out olen)))
|
||||
(values (soxr-error-ptr->string err) idone odone)))
|
||||
|
||||
) ; end of module
|
||||
+1
-1
@@ -60,7 +60,7 @@
|
||||
|
||||
(define zlib (get-lib '("zlib" "libz") (linux-lib-versions '("1" #f))))
|
||||
(define libtag (get-lib '("tag" "libtag") (linux-lib-versions '("2" #f) '("2" #f))))
|
||||
(define libtag_c (get-lib '("tag_c" "libtag_c") (linux-lib-versions '("2" #f) '("#2" #f))))
|
||||
(define libtag_c (get-lib '("tag_c" "libtag_c") (linux-lib-versions '("2" #f) '("2" #f))))
|
||||
|
||||
(define-ffi-definer define-tag-c-lib libtag_c
|
||||
#:default-make-fail make-not-available)
|
||||
|
||||
Reference in New Issue
Block a user