Encoder testing

This commit is contained in:
2026-06-08 12:14:32 +02:00
parent 444d62edac
commit d6aa880104
6 changed files with 261 additions and 61 deletions
+56 -32
View File
@@ -11,12 +11,29 @@
opus-encoder-finish)
;; libopusenc handles the Ogg container, OpusHead and OpusTags. The Racket
;; side only feeds interleaved signed 16-bit PCM to ope_encoder_write().
;; side feeds interleaved floating-point PCM to ope_encoder_write_float().
;; The input rate passed to ope_encoder_create_file is the source PCM rate;
;; libopusenc performs the required Opus resampling internally.
;; Load libogg and libopus explicitly before libopusenc. This matters on
;; Windows, where libopusenc.dll may not reliably find its dependent DLLs
;; unless they have already been resolved through the same search path.
(define libogg
(get-lib (case (system-type 'os)
[(windows) '("ogg")]
[else '("ogg" "libogg")])
'(#f)))
(define libopus
(get-lib (case (system-type 'os)
[(windows) '("opus")]
[else '("opus" "libopus")])
'(#f)))
(define libopusenc
(get-lib (case (system-type 'os)
[(windows) '("opusenc")]
[else '("opusenc" "libopusenc")])
[(windows) '("libopusenc")]
[else '("opusenc" "libopusenc")])
'(#f)))
(define _OggOpusComments (_cpointer/null 'ogg-opus-comments))
@@ -37,7 +54,7 @@
(_fun _string/utf-8 _OggOpusComments _int32 _int _int (err : (_ptr o _int))
-> (enc : _OggOpusEnc)
-> (values enc err))))
(define ope_encoder_write (ffi-proc "ope_encoder_write" (_fun _OggOpusEnc _bytes _int -> _int)))
(define ope_encoder_write_float (ffi-proc "ope_encoder_write_float" (_fun _OggOpusEnc _pointer _int -> _int)))
(define ope_encoder_drain (ffi-proc "ope_encoder_drain" (_fun _OggOpusEnc -> _int)))
(define ope_encoder_destroy (ffi-proc "ope_encoder_destroy" (_fun _OggOpusEnc -> _void)))
(define ope_strerror (ffi-proc "ope_strerror" (_fun _int -> _string/utf-8)))
@@ -56,8 +73,8 @@
(define OPUS_SIGNAL_MUSIC 3002)
(define (opus-encoder-available?)
(and libopusenc ope_comments_create ope_comments_destroy ope_encoder_create_file
ope_encoder_write ope_encoder_drain ope_encoder_destroy ope_strerror #t))
(and libogg libopus libopusenc ope_comments_create ope_comments_destroy ope_encoder_create_file
ope_encoder_write_float ope_encoder_drain ope_encoder_destroy ope_strerror #t))
(define-struct opus-encoder-handle (enc comments settings format file) #:transparent)
@@ -89,21 +106,24 @@
(complexity . 10)
(comment-padding . 512))))
(define (valid-opus-rate? rate)
(or (= rate 8000) (= rate 12000) (= rate 16000) (= rate 24000) (= rate 48000)))
(define (signal->int v)
(cond [(or (eq? v 'auto) (eq? v #f)) OPUS_AUTO]
[(eq? v 'voice) OPUS_SIGNAL_VOICE]
[(eq? v 'music) OPUS_SIGNAL_MUSIC]
[else (raise-argument-error 'opus-signal "(or/c 'auto 'voice 'music)" v)]))
(define (source-value v source)
(if (eq? v 'source) source v))
(define (opus-encoder-prepare-settings settings format)
(let* ((h (hash-merge (opus-encoder-default-settings) settings))
(rate (hash-ref/default h 'sample-rate (hash-ref format 'sample-rate)))
(channels (hash-ref/default h 'channels (hash-ref format 'channels))))
(unless (valid-opus-rate? rate)
(error 'opus-encoder-open "Opus input sample rate must be 8000, 12000, 16000, 24000 or 48000 Hz; got ~a. Resample before calling libopusenc." rate))
(rate (source-value (hash-ref/default h 'sample-rate (hash-ref format 'sample-rate))
(hash-ref format 'sample-rate)))
(channels (source-value (hash-ref/default h 'channels (hash-ref format 'channels))
(hash-ref format 'channels))))
;; Do not apply the low-level libopus sample-rate restriction here.
;; libopusenc accepts the input rate and performs the required resampling
;; internally; 44100 Hz input is therefore valid.
(when (> channels 2)
(error 'opus-encoder-open "this first direct libopusenc backend only supports mono/stereo input; got ~a channels" channels))
(hash-set! h 'sample-rate rate)
@@ -137,7 +157,8 @@
(hash-keys ch))))))
(define (opus-encoder-open output-file settings format)
(unless (opus-encoder-available?) (error 'opus-encoder-open "libopusenc could not be loaded"))
(unless (opus-encoder-available?)
(error 'opus-encoder-open "libopusenc or one of its dependent libraries (ogg/opus) could not be loaded"))
(let* ((file (if (path? output-file) (path->string output-file) output-file))
(resolved (opus-encoder-prepare-settings settings format))
(comments (ope_comments_create)))
@@ -151,35 +172,38 @@
(make-opus-encoder-handle enc comments resolved format file))))
(define (native-signed-ref bs start bytes)
(integer-bytes->integer bs #t (system-big-endian?) start (+ start bytes)))
;; Racket's integer-bytes->integer only supports 1, 2, 4 and 8 bytes.
;; The FLAC decoder legitimately produces 24-bit PCM as three bytes per
;; sample, so use the package helper that handles that case.
(int-bytes->integer bs #t (system-big-endian?) start (+ start bytes)))
(define (sample->s16 sample in-bits)
(cond [(> in-bits 16) (arithmetic-shift sample (- 16 in-bits))]
[(< in-bits 16) (arithmetic-shift sample (- 16 in-bits))]
[else sample]))
(define (sample->float sample in-bits)
(let* ((scale (expt 2 (sub1 in-bits)))
(v (/ sample scale)))
(cond [(< v -1.0) -1.0]
[(> v 1.0) 1.0]
[else (exact->inexact v)])))
(define (write-s16-native! out offset sample)
(integer->integer-bytes sample 2 #t (system-big-endian?) out offset))
(define (pcm-bytes->s16 buffer size in-bits)
(define (pcm-bytes->float-pointer buffer size in-bits)
(let* ((in-bytes (quotient in-bits 8))
(sample-count (quotient size in-bytes))
(out (make-bytes (* sample-count 2))))
(ptr (malloc _float sample-count 'atomic-interior)))
(for ([i (in-range sample-count)])
(let* ((in-off (* i in-bytes))
(out-off (* i 2))
(sample (native-signed-ref buffer in-off in-bytes)))
(write-s16-native! out out-off (sample->s16 sample in-bits))))
out))
(ptr-set! ptr _float i (sample->float sample in-bits))))
(values ptr sample-count)))
(define (opus-encoder-write handle buf-info buffer buf-len)
(let* ((settings (opus-encoder-handle-settings handle))
(channels (hash-ref settings 'channels))
(in-bits (hash-ref/default buf-info 'bits-per-sample 16))
(pcm (if (= in-bits 16) buffer (pcm-bytes->s16 buffer buf-len in-bits)))
(frames (quotient (quotient (bytes-length pcm) 2) channels)))
(check-ope 'opus-encoder-write (ope_encoder_write (opus-encoder-handle-enc handle) pcm frames))
frames))
(in-bits (hash-ref/default buf-info 'pcm-bits-per-sample
(hash-ref/default buf-info 'bits-per-sample 16))))
(let-values (((pcm sample-count) (pcm-bytes->float-pointer buffer buf-len in-bits)))
(let ((frames (quotient sample-count channels)))
(check-ope 'opus-encoder-write
(ope_encoder_write_float (opus-encoder-handle-enc handle) pcm frames))
frames))))
(define (opus-encoder-finish handle)
(dynamic-wind