diff --git a/README.md b/README.md index 1642d3b..2588a32 100644 --- a/README.md +++ b/README.md @@ -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 ``` diff --git a/libao-async-ffi-racket.rkt b/libao-async-ffi-racket.rkt index 02b9342..be5cc0c 100644 --- a/libao-async-ffi-racket.rkt +++ b/libao-async-ffi-racket.rkt @@ -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)) diff --git a/private/pcm-converter.rkt b/private/pcm-converter.rkt index 37d1a5c..85d2acc 100644 --- a/private/pcm-converter.rkt +++ b/private/pcm-converter.rkt @@ -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) diff --git a/private/utils.rkt b/private/utils.rkt index 6cd478c..92a28a0 100644 --- a/private/utils.rkt +++ b/private/utils.rkt @@ -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 " diff --git a/resampler.rkt b/resampler.rkt new file mode 100644 index 0000000..bb13ec5 --- /dev/null +++ b/resampler.rkt @@ -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 diff --git a/scrbl/audio-encoder.scrbl b/scrbl/audio-encoder.scrbl index db93abc..06f017c 100644 --- a/scrbl/audio-encoder.scrbl +++ b/scrbl/audio-encoder.scrbl @@ -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: diff --git a/scrbl/racket-audio.scrbl b/scrbl/racket-audio.scrbl index 465a386..4fb5ffc 100644 --- a/scrbl/racket-audio.scrbl +++ b/scrbl/racket-audio.scrbl @@ -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"] diff --git a/scrbl/resampler.scrbl b/scrbl/resampler.scrbl new file mode 100644 index 0000000..8a273d7 --- /dev/null +++ b/scrbl/resampler.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. +} diff --git a/soxr-ffi.rkt b/soxr-ffi.rkt new file mode 100644 index 0000000..bdb7ae0 --- /dev/null +++ b/soxr-ffi.rkt @@ -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 diff --git a/taglib-ffi.rkt b/taglib-ffi.rkt index f56acfc..c2f8ad9 100644 --- a/taglib-ffi.rkt +++ b/taglib-ffi.rkt @@ -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)