This commit is contained in:
2026-06-10 01:42:08 +02:00
parent 4f95e57a96
commit 5c719ea4a0
10 changed files with 681 additions and 72 deletions
+5 -5
View File
@@ -14,8 +14,7 @@ backends:
- libao - libao
- libFLAC - libFLAC
- mpg123 - mpg123
- FFmpeg libraries, including libavutil, libavcodec, libavformat and - FFmpeg libraries, including libavutil, libavcodec and libavformat
libswresample
For encoding, also install: For encoding, also install:
@@ -23,10 +22,10 @@ For encoding, also install:
- libopus - libopus
- libogg - libogg
- TagLib with the C binding, usually provided as `taglib` / `taglib_c` - 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 The Opus encoder backend uses libopusenc directly. The FLAC encoder backend
uses libFLAC directly. FLAC sample-rate conversion uses the existing FFmpeg uses libFLAC directly. FLAC sample-rate conversion uses `resampler.rkt`, backed by libsoxr. Metadata and cover-art copying use the TagLib wrapper; the
swresample layer. Metadata and cover-art copying use the TagLib wrapper; the
public `taglib.rkt` API also supports read-write tag editing. public `taglib.rkt` API also supports read-write tag editing.
@@ -43,7 +42,7 @@ sudo apt install \
ffmpeg \ ffmpeg \
libavutil-dev libavcodec-dev libavformat-dev libswresample-dev \ libavutil-dev libavcodec-dev libavformat-dev libswresample-dev \
libogg-dev libopus-dev libopusenc-dev libopusfile-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 For Opus encoding, `libopusenc-dev` is required. For Opus decoding through
@@ -60,6 +59,7 @@ brew install mpg123
brew install ffmpeg brew install ffmpeg
brew install opus brew install opus
brew install libopusenc brew install libopusenc
brew install libsoxr
brew install taglib brew install taglib
``` ```
+2 -1
View File
@@ -68,7 +68,8 @@
[else "libao"])) [else "libao"]))
(linux-lib-versions '("4" #f)))) (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-device (_cpointer/null 'ao-device))
(define _ao-option (_cpointer/null 'ao-option)) (define _ao-option (_cpointer/null 'ao-option))
+21 -58
View File
@@ -2,7 +2,7 @@
(require ffi/unsafe (require ffi/unsafe
"utils.rkt" "utils.rkt"
"../ffmpeg-definitions.rkt") "../resampler.rkt")
(provide pcm-conversion-needed? (provide pcm-conversion-needed?
make-pcm-converter make-pcm-converter
@@ -15,7 +15,7 @@
(define S32-BYTES 4) (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 #:mutable
#:constructor-name make-raw-pcm-converter) #:constructor-name make-raw-pcm-converter)
@@ -32,7 +32,7 @@
(int-bytes->integer bs #t (system-big-endian?) start (+ start bytes))) (int-bytes->integer bs #t (system-big-endian?) start (+ start bytes)))
(define (native-signed-set! bs start bytes value) (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) (define (clamp-s32 v)
(cond [(< v -2147483648) -2147483648] (cond [(< v -2147483648) -2147483648]
@@ -55,21 +55,6 @@
(native-signed-set! out out-off S32-BYTES (expand-sample-to-s32 sample in-bits)))) (native-signed-set! out out-off S32-BYTES (expand-sample-to-s32 sample in-bits))))
out)])) 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) (define (source-value v source)
(if (eq? v 'source) source v)) (if (eq? v 'source) source v))
@@ -123,36 +108,26 @@
(channels-out (target-channels settings input-format)) (channels-out (target-channels settings input-format))
(rate-in (hash-ref input-format 'sample-rate)) (rate-in (hash-ref input-format 'sample-rate))
(rate-out (target-sample-rate settings input-format)) (rate-out (target-sample-rate settings input-format))
(in-layout (ffmpeg-make-default-channel-layout channels-in)) (quality (hash-ref/default settings 'resampler-quality 'hq))
(out-layout (ffmpeg-make-default-channel-layout channels-out))) (phase (hash-ref/default settings 'resampler-phase 'linear))
(let-values (((ret ctx) (swr_alloc_set_opts2 #f (steep? (hash-ref/default settings 'resampler-steep-filter? #f))
out-layout AV_SAMPLE_FMT_S32 rate-out (out-format (make-output-format input-format settings)))
in-layout AV_SAMPLE_FMT_S32 rate-in (unless (= channels-in channels-out)
0 #f))) (error 'make-pcm-converter
(when (< ret 0) (error 'make-pcm-converter "swr_alloc_set_opts2 failed: ~a" ret)) "SoXR PCM converter only supports unchanged channel count; got input ~a and output ~a"
(let ((ret-init (swr_init ctx))) channels-in channels-out))
(when (< ret-init 0) (error 'make-pcm-converter "swr_init failed: ~a" ret-init)) (let ((r (make-resampler rate-in rate-out channels-in
(make-raw-pcm-converter ctx in-layout out-layout input-format (make-output-format input-format settings) #:input-format 's32
channels-out rate-in rate-out #f))))) #: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) (define (ensure-open! c who)
(when (or (not (pcm-converter? c)) (pcm-converter-closed? c)) (when (or (not (pcm-converter? c)) (pcm-converter-closed? c))
(error who "PCM converter is closed"))) (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) (define (pcm-converter-convert c buffer size buf-info)
(ensure-open! c 'pcm-converter-convert) (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))) (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-channels (hash-ref (pcm-converter-input-format c) 'channels))
(in-samples (quotient (quotient size in-bytes) in-channels)) (in-samples (quotient (quotient size in-bytes) in-channels))
(s32 (pcm-bytes->s32-bytes buffer size in-bits))) (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) (define (pcm-converter-drain c)
(ensure-open! c 'pcm-converter-drain) (ensure-open! c 'pcm-converter-drain)
(let* ((ctx (pcm-converter-swr-ctx c)) (resampler-drain (pcm-converter-resampler 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))])))
(define (pcm-converter-close! c) (define (pcm-converter-close! c)
(when (and (pcm-converter? c) (not (pcm-converter-closed? c))) (when (and (pcm-converter? c) (not (pcm-converter-closed? c)))
(set-pcm-converter-swr-ctx! c (swr_free (pcm-converter-swr-ctx c))) (resampler-close! (pcm-converter-resampler c))
(ffmpeg-channel-layout-uninit! (pcm-converter-in-layout c))
(ffmpeg-channel-layout-uninit! (pcm-converter-out-layout c))
(set-pcm-converter-closed?! c #t)) (set-pcm-converter-closed?! c #t))
#t) #t)
+6 -4
View File
@@ -178,14 +178,14 @@
(loop (+ n 1) (cons (number->string n) acc))))) (loop (+ n 1) (cons (number->string n) acc)))))
(define (ffmpeg-lib-versions kind) (define (ffmpeg-lib-versions kind)
(case (system-type 'os) (case (system-type 'os*)
[(linux) [(linux)
(let ((v (hash-ref valid-ffmpeg-versions kind))) (let ((v (hash-ref valid-ffmpeg-versions kind)))
(versions-high-to-low (car v) (cadr v)))] (versions-high-to-low (car v) (cadr v)))]
[else '(#f)])) [else '(#f)]))
(define (linux-lib-versions versions [default '(#f)]) (define (linux-lib-versions versions [default '(#f)])
(case (system-type 'os) (case (system-type 'os*)
[(linux) versions] [(linux) versions]
[else default])) [else default]))
@@ -213,7 +213,7 @@
" "
" libavcodec60 libavutil58 libswresample4 libavformat60 \ " 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, "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 opus
" "
" brew install libopusenc " brew install libopusenc
"
" brew install libsoxr
" "
" brew install taglib " brew install taglib
" "
+285
View File
@@ -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
+4 -3
View File
@@ -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 The FLAC backend uses the @tt{libFLAC} stream encoder. It writes interleaved
integer PCM samples through the FLAC encoder API. When the requested output 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] sample rate differs from the decoded input format, @racketmodname[racket-audio/private/pcm-converter]
uses the existing FFmpeg @tt{swresample} layer from uses @racketmodname[racket-audio/resampler], backed by @tt{libsoxr}, to perform
@racketmodname[racket-audio/ffmpeg-definitions] to perform PCM normalisation. 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: The following settings are recognised:
+1
View File
@@ -22,6 +22,7 @@
@include-section["audio-sniffer.scrbl"] @include-section["audio-sniffer.scrbl"]
@include-section["taglib.scrbl"] @include-section["taglib.scrbl"]
@include-section["audio-encoder.scrbl"] @include-section["audio-encoder.scrbl"]
@include-section["resampler.scrbl"]
@include-section["encoder-test.scrbl"] @include-section["encoder-test.scrbl"]
@include-section["play-test.scrbl"] @include-section["play-test.scrbl"]
@include-section["audio-placed-player.scrbl"] @include-section["audio-placed-player.scrbl"]
+101
View File
@@ -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
View File
@@ -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
View File
@@ -60,7 +60,7 @@
(define zlib (get-lib '("zlib" "libz") (linux-lib-versions '("1" #f)))) (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 (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 (define-ffi-definer define-tag-c-lib libtag_c
#:default-make-fail make-not-available) #:default-make-fail make-not-available)