Documentation added.

This commit is contained in:
2026-06-08 13:16:21 +02:00
parent 8e8b9a00c0
commit 5eefacacba
7 changed files with 610 additions and 35 deletions
+72 -7
View File
@@ -1,14 +1,79 @@
# racket-audio
Integration of common audio libraries in racket.
Integration of common audio libraries in Racket.
## Mac OS X
The package contains decoder, player and encoder bindings. Playback uses the
existing audio player modules. Encoding is provided by `audio-encoder.rkt` with
Opus and FLAC backends.
Make sure you have libao, libFLAC, mpg123 and ffmpeg-full installed using brew.
## Native dependencies
% brew install libao
% brew install flac
% brew install mpg123
% brew install ffmpeg-full
For playback and decoding, install the native libraries used by the selected
backends:
- libao
- libFLAC
- mpg123
- FFmpeg libraries, including libavutil, libavcodec, libavformat and
libswresample
For encoding, also install:
- libopusenc
- libopus
- libogg
- TagLib with the C binding, usually provided as `taglib` / `taglib_c`
The Opus encoder backend uses libopusenc directly. The FLAC encoder backend
uses libFLAC directly. FLAC sample-rate conversion uses the existing FFmpeg
swresample layer.
## macOS
Using Homebrew, install the native libraries before using the package:
```sh
brew install libao
brew install flac
brew install mpg123
brew install ffmpeg
brew install opus
brew install libopusenc
brew install taglib
```
Some Homebrew installations provide FFmpeg as `ffmpeg`; older local setups may
use `ffmpeg-full`.
## Encoder examples
Encode to Opus:
```racket
(require "audio-encoder.rkt")
(audio-encode "input.flac"
"output.opus"
(hash 'bitrate 224000
'vbr? #t
'complexity 10)
#:encoder 'opus)
```
Encode 96 kHz FLAC to 48 kHz FLAC:
```racket
(audio-encode "input-96k.flac"
"output-48k.flac"
(hash 'sample-rate 48000
'bits-per-sample 24
'compression-level 8)
#:encoder 'flac)
```
A small test wrapper is available in `encoder-test.rkt`:
```sh
racket encoder-test.rkt --encoder opus --input input.flac --output output.opus --bitrate-kbps 224
racket encoder-test.rkt --encoder flac --input input-96k.flac --output output-48k.flac --sample-rate 48000
```
+121 -14
View File
@@ -68,18 +68,74 @@
(for-each (lambda (k) (hash-set! out k (hash-ref b k))) (hash-keys b)))
out))
(define (copy-hash h)
(let ((out (make-hash)))
(when (hash? h)
(for-each (lambda (k) (hash-set! out k (hash-ref h k))) (hash-keys h)))
out))
(define (maybe-string s) (and (string? s) (not (string=? s "")) s))
(define (maybe-number n) (and (number? n) (>= n 0) (number->string n)))
(define (source-tags->opus-settings input-file settings)
;; For Opus, embedded pictures must be written into the OpusTags packet
;; before the encoder starts. TagLib post-processing is not reliable for
;; this path, so transfer the regular comments and cover art through
;; libopusenc comments instead.
(with-handlers ([exn:fail? (lambda (e)
(warn-sound "Could not read source tags from ~a for Opus comments: ~a"
input-file (exn-message e))
settings)])
(call-with-id3-tags
input-file
(lambda (src)
(if (not (tags-valid? src))
settings
(let ((out (copy-hash settings))
(comments (make-hash)))
(let ((title (maybe-string (tags-title src)))) (when title (hash-set! comments 'title title)))
(let ((album (maybe-string (tags-album src)))) (when album (hash-set! comments 'album album)))
(let ((artist (maybe-string (tags-artist src)))) (when artist (hash-set! comments 'artist artist)))
(let ((comment (maybe-string (tags-comment src)))) (when comment (hash-set! comments 'comment comment)))
(let ((genre (maybe-string (tags-genre src)))) (when genre (hash-set! comments 'genre genre)))
(let ((composer (maybe-string (tags-composer src)))) (when composer (hash-set! comments 'composer composer)))
(let ((album-artist (maybe-string (tags-album-artist src)))) (when album-artist (hash-set! comments 'albumartist album-artist)))
(let ((year (maybe-number (tags-year src)))) (when year (hash-set! comments 'date year)))
(let ((track (maybe-number (tags-track src)))) (when track (hash-set! comments 'tracknumber track)))
(let ((disc (tags-disc-number src)))
(cond [(string? disc) (unless (string=? disc "") (hash-set! comments 'discnumber disc))]
[(and (number? disc) (>= disc 0)) (hash-set! comments 'discnumber (number->string disc))]
[else (void)]))
(unless (null? (hash-keys comments)) (hash-set! out 'comments comments))
(let ((picture (tags-picture src)))
(unless (eq? picture #f) (hash-set! out 'picture picture)))
out)))
#:mode 'read)))
(define (make-tag-result method success? picture note)
(let ((h (make-hash)))
(hash-set! h 'method method)
(hash-set! h 'success? success?)
(hash-set! h 'picture? (not (eq? picture #f)))
(when (id3-picture? picture)
(hash-set! h 'picture-size (id3-picture-size picture))
(hash-set! h 'picture-mimetype (id3-picture-mimetype picture)))
(when note (hash-set! h 'note note))
h))
(define (copy-tags! input-file output-file)
(with-handlers ([exn:fail? (lambda (e)
(warn-sound "Could not copy tags from ~a to ~a: ~a"
input-file output-file (exn-message e))
#f)])
(make-tag-result 'taglib-post-copy #f #f (exn-message e)))])
(call-with-id3-tags
input-file
(lambda (src)
(call-with-id3-tags
output-file
(lambda (dst)
(when (and (tags-valid? src) (tags-valid? dst))
(if (and (tags-valid? src) (tags-valid? dst))
(begin
(tag-value-copy! src dst tags-title tags-title! empty-string?)
(tag-value-copy! src dst tags-album tags-album! empty-string?)
(tag-value-copy! src dst tags-artist tags-artist! empty-string?)
@@ -91,29 +147,69 @@
(tag-value-copy! src dst tags-track tags-track! empty-number?)
(tag-value-copy! src dst tags-disc-number tags-disc-number! empty-number?)
(let ((picture (tags-picture src)))
(unless (eq? picture #f) (tags-picture! dst picture)))
(tags-save! dst)))
(unless (eq? picture #f) (tags-picture! dst picture))
(tags-save! dst)
(make-tag-result 'taglib-post-copy #t picture #f)))
(make-tag-result 'taglib-post-copy #f #f "source or destination tags invalid")))
#:mode 'read-write))
#:mode 'read)
#t))
#:mode 'read)))
(define (input-frames-in-buffer fmt buf-len)
(let* ((channels (hash-ref fmt 'channels 1))
(bits (hash-ref fmt 'bits-per-sample (hash-ref fmt 'pcm-bits-per-sample 16)))
(bytes-per-sample (max 1 (quotient bits 8)))
(frame-bytes (* channels bytes-per-sample)))
(if (> frame-bytes 0) (quotient buf-len frame-bytes) 0)))
(define (total-input-frames fmt)
(and (hash? fmt)
(or (hash-ref fmt 'total-samples #f)
(hash-ref fmt 'total-frames #f)
(hash-ref fmt 'frames #f))))
(define (audio-encode input-file output-file settings
#:encoder [explicit-kind #f]
#:copy-tags? [copy-tags? #t])
#:copy-tags? [copy-tags? #t]
#:progress-callback [progress-callback #f])
(define-values (kind encoder) (encoder-for-output output-file explicit-kind))
(define effective-settings (if (and copy-tags? (eq? kind 'opus))
(source-tags->opus-settings input-file settings)
settings))
(define backend-handle #f)
(define format #f)
(define output-format #f)
(define converter #f)
(define frames-written 0)
(define frames-read 0)
(define last-progress -1.0)
(define tags-result #f)
(define (progress! phase input-format)
(when progress-callback
(let* ((total (total-input-frames input-format))
(progress (and (integer? total) (> total 0)
(min 1.0 (/ frames-read total))))
(h (make-hash)))
(hash-set! h 'phase phase)
(hash-set! h 'encoder kind)
(hash-set! h 'input input-file)
(hash-set! h 'output output-file)
(hash-set! h 'frames-read frames-read)
(hash-set! h 'frames-written frames-written)
(hash-set! h 'total-frames total)
(hash-set! h 'progress progress)
(hash-set! h 'input-format input-format)
(when output-format (hash-set! h 'output-format output-format))
(progress-callback h)
(when (number? progress) (set! last-progress progress)))))
(define (ensure-open! fmt)
(when (eq? backend-handle #f)
;; Record the resolved output format, not merely the incoming PCM format.
;; This matters when only FLAC bit depth changes, because no swresample
;; converter is needed but the resulting FLAC stream metadata still differs.
(set! output-format ((audio-encoder-settings encoder) settings fmt))
(set! backend-handle ((audio-encoder-open encoder) output-file settings fmt))))
(set! output-format ((audio-encoder-settings encoder) effective-settings fmt))
(set! backend-handle ((audio-encoder-open encoder) output-file effective-settings fmt))))
(define (write-backend! fmt buffer buf-len)
(ensure-open! fmt)
@@ -125,8 +221,8 @@
;; converter by default: libopusenc accepts the source input rate and has
;; its own resampler, and opus-encoder.rkt feeds it float PCM directly.
(when (and (eq? kind 'flac) (eq? converter #f))
(when (pcm-conversion-needed? input-format settings)
(set! converter (make-pcm-converter input-format settings)))))
(when (pcm-conversion-needed? input-format effective-settings)
(set! converter (make-pcm-converter input-format effective-settings)))))
(define (write-converted! input-format buffer buf-len)
(ensure-converter! input-format)
@@ -148,12 +244,15 @@
;; Keep stream metadata, but delay encoder creation until the first audio
;; buffer. Some decoders report an output-oriented stream format first
;; and then the exact PCM frame format in buf-info.
(set! format fmt))
(set! format fmt)
(progress! 'format fmt))
(define (on-audio audio-kind ao-kind handle buf-info buffer buf-len)
(let ((effective-format (merge-hash format buf-info)))
(set! format effective-format)
(write-converted! effective-format buffer buf-len)))
(set! frames-read (+ frames-read (input-frames-in-buffer effective-format buf-len)))
(write-converted! effective-format buffer buf-len)
(progress! 'audio effective-format)))
(let* ((audio-open-proc (dynamic-require audio-decoder-module 'audio-open))
(audio-read-proc (dynamic-require audio-decoder-module 'audio-read))
@@ -167,14 +266,22 @@
(lambda () (when backend-handle ((audio-encoder-finish encoder) backend-handle)))
(lambda () (when converter (pcm-converter-close! converter)))))))
(when copy-tags? (copy-tags! input-file output-file))
(progress! 'finished-encoding format)
(set! tags-result
(cond [(not copy-tags?) (make-tag-result 'none #t #f "tag copy disabled")]
[(eq? kind 'opus)
(make-tag-result 'libopusenc-comments #t (hash-ref effective-settings 'picture #f) #f)]
[else (copy-tags! input-file output-file)]))
(progress! 'finished format)
(let ((r (make-hash)))
(hash-set! r 'encoder kind)
(hash-set! r 'input input-file)
(hash-set! r 'output output-file)
(hash-set! r 'input-format format)
(hash-set! r 'output-format output-format)
(hash-set! r 'frames-read frames-read)
(hash-set! r 'frames-written frames-written)
(hash-set! r 'tag-copy tags-result)
r))
) ; end of module
+31 -2
View File
@@ -58,7 +58,18 @@
(hash-ref fmt 'sample-rate "?")
(hash-ref fmt 'channels "?")
(hash-ref fmt 'bits-per-sample "?")
(hash-ref fmt 'total-frames "?"))
(hash-ref fmt 'total-frames (hash-ref fmt 'total-samples "?")))
"unknown"))
(define (tag-summary tag-copy)
(if (hash? tag-copy)
(format "method=~a, success=~a, picture=~a~a"
(hash-ref tag-copy 'method "?")
(hash-ref tag-copy 'success? "?")
(hash-ref tag-copy 'picture? #f)
(let ((size (hash-ref tag-copy 'picture-size #f))
(mt (hash-ref tag-copy 'picture-mimetype #f)))
(if size (format ", ~a bytes, ~a" size mt) "")))
"unknown"))
(define (display-result result)
@@ -68,11 +79,26 @@
(displayln (format "encoder : ~a" (hash-ref result 'encoder '?)))
(displayln (format "input : ~a" (hash-ref result 'input '?)))
(displayln (format "output : ~a" (hash-ref result 'output '?)))
(displayln (format "frames read : ~a" (hash-ref result 'frames-read '?)))
(displayln (format "frames written : ~a" (hash-ref result 'frames-written '?)))
(displayln (format "input format : ~a" (format-summary (hash-ref result 'input-format #f))))
(displayln (format "output format : ~a" (format-summary (hash-ref result 'output-format #f))))
(displayln (format "tag copy : ~a" (tag-summary (hash-ref result 'tag-copy #f))))
result)
(define (make-progress-callback)
(define last-pct -1)
(lambda (h)
(let ((p (hash-ref h 'progress #f)))
(when (number? p)
(let ((pct (inexact->exact (round (* 100 p)))))
(when (not (= pct last-pct))
(set! last-pct pct)
(printf "\rprogress : ~a%" pct)
(flush-output))
(when (or (>= pct 100) (eq? (hash-ref h 'phase #f) 'finished))
(newline)))))))
(define (encoder-test input-file output-file encoder settings #:copy-tags? [copy-tags? #t])
(let* ((enc (encoder-symbol encoder))
(out (if output-file output-file (default-output-file enc))))
@@ -81,7 +107,10 @@
(displayln (format " -> ~a" out))
(displayln (format "encoder : ~a" enc))
(displayln (format "settings: ~a" settings))
(display-result (audio-encode input-file out settings #:encoder enc #:copy-tags? copy-tags?))))
(display-result (audio-encode input-file out settings
#:encoder enc
#:copy-tags? copy-tags?
#:progress-callback (make-progress-callback)))))
(define (encoder-test-opus [input-file test-file3]
[output-file #f]
+52 -1
View File
@@ -1,7 +1,9 @@
(module opus-encoder racket/base
(require ffi/unsafe
"private/utils.rkt")
racket/string
"private/utils.rkt"
"taglib.rkt")
(provide opus-encoder-available?
opus-encoder-default-settings
@@ -156,6 +158,54 @@
(check-ope 'opus-comment (ope_comments_add comments (string-upcase (symbol->string k)) v)))))
(hash-keys ch))))))
(define (picture-kind->opus-int kind)
(define s
(cond [(number? kind) (number->string kind)]
[(symbol? kind) (string-replace (string-downcase (symbol->string kind)) "-" " ")]
[(string? kind) (string-downcase kind)]
[else ""]))
(cond [(or (string=? s "0") (string=? s "other")) 0]
[(or (string=? s "1") (string=? s "file icon") (string=? s "32x32 icon")) 1]
[(or (string=? s "2") (string=? s "other file icon")) 2]
[(or (string=? s "3") (string=? s "front cover") (string=? s "cover front")
(string=? s "cover (front)") (string=? s "front")) 3]
[(or (string=? s "4") (string=? s "back cover") (string=? s "cover back")
(string=? s "cover (back)") (string=? s "back")) 4]
[(or (string=? s "5") (string=? s "leaflet page")) 5]
[(or (string=? s "6") (string=? s "media") (string=? s "label side of media")) 6]
[(or (string=? s "7") (string=? s "lead artist") (string=? s "lead performer")
(string=? s "soloist")) 7]
[(or (string=? s "8") (string=? s "artist") (string=? s "performer")) 8]
[(or (string=? s "9") (string=? s "conductor")) 9]
[(or (string=? s "10") (string=? s "band") (string=? s "orchestra")) 10]
[(or (string=? s "11") (string=? s "composer")) 11]
[(or (string=? s "12") (string=? s "lyricist") (string=? s "text writer")) 12]
[(or (string=? s "13") (string=? s "recording location")) 13]
[(or (string=? s "14") (string=? s "during recording")) 14]
[(or (string=? s "15") (string=? s "during performance")) 15]
[(or (string=? s "16") (string=? s "movie screen capture")) 16]
[(or (string=? s "17") (string=? s "a bright coloured fish")
(string=? s "bright coloured fish")) 17]
[(or (string=? s "18") (string=? s "illustration")) 18]
[(or (string=? s "19") (string=? s "band logo") (string=? s "artist logotype")) 19]
[(or (string=? s "20") (string=? s "publisher logo") (string=? s "publisher logotype")) 20]
[else 3]))
(define (add-picture! comments settings)
(when (hash-has-key? settings 'picture)
(unless ope_comments_add_picture_from_memory
(error 'opus-picture "libopusenc does not provide ope_comments_add_picture_from_memory"))
(let ((picture (hash-ref settings 'picture)))
(when (id3-picture? picture)
(let ((data (id3-picture-bytes picture)))
(check-ope 'opus-picture
(ope_comments_add_picture_from_memory
comments
data
(bytes-length data)
(picture-kind->opus-int (id3-picture-kind picture))
(id3-picture-description picture))))))))
(define (opus-encoder-open output-file settings format)
(unless (opus-encoder-available?)
(error 'opus-encoder-open "libopusenc or one of its dependent libraries (ogg/opus) could not be loaded"))
@@ -163,6 +213,7 @@
(resolved (opus-encoder-prepare-settings settings format))
(comments (ope_comments_create)))
(add-comments! comments resolved)
(add-picture! comments resolved)
(let-values (((enc err) (ope_encoder_create_file file comments
(hash-ref resolved 'sample-rate)
(hash-ref resolved 'channels)
+230
View File
@@ -0,0 +1,230 @@
#lang scribble/manual
@(require (for-label racket/base
racket/contract
racket/path
"../audio-encoder.rkt"))
@title{Audio Encoding}
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
@defmodule[racket-audio/audio-encoder]
The @racketmodname[racket-audio/audio-encoder] module provides the high level
file-to-file encoding pipeline. It reuses the existing decoder environment to
read the input file and sends the decoded PCM stream to a selected encoder
backend. The built-in backends are Opus, implemented with @tt{libopusenc}, and
FLAC, implemented with @tt{libFLAC}.
This module is intended as the public encoding API. The concrete backend
modules are small FFI backends; applications normally call @racket[audio-encode]
instead of using those modules directly.
@section{Pipeline}
Encoding is organised as a streaming pipeline:
@racketblock[
input file
;; decoded by audio-decoder.rkt
-> PCM buffers
;; optional conversion for FLAC
-> encoder backend
-> output file]
The encoder is selected from @racket[#:encoder] or, when that argument is not
provided, from the output filename extension. The initial built-in encoders are
@racket['opus] for @filepath{.opus} and @filepath{.oga} files, and
@racket['flac] for @filepath{.flac} files.
The PCM stream is not collected in memory. Each decoded buffer is forwarded to
the selected backend. FLAC encoding may insert a PCM conversion step when the
settings request a different sample rate, channel count, or bit depth. Opus
encoding feeds floating-point PCM to @tt{libopusenc}; sample-rate conversion for
Opus is left to @tt{libopusenc}.
@section{Encoding a file}
@defproc[(audio-encode [input-file path-string?]
[output-file path-string?]
[settings hash?]
[#:encoder encoder (or/c symbol? #f) #f]
[#:copy-tags? copy-tags? boolean? #t]
[#:progress-callback progress-callback
(or/c procedure? #f) #f])
hash?]{
Encodes @racket[input-file] to @racket[output-file] and returns a result hash.
The @racket[settings] hash is interpreted by the selected backend.
When @racket[encoder] is @racket[#f], the backend is inferred from the output
file extension. Pass @racket['opus] or @racket['flac] to force a backend.
When @racket[copy-tags?] is true, common textual tags and an embedded picture
are copied from the source file to the destination file. Opus comments and
cover art are written before encoding starts through @tt{libopusenc}. FLAC
metadata is copied after the encoded file has been written, using the TagLib
wrapper.
When @racket[progress-callback] is a procedure, it is called with a progress
hash during encoding. Progress is based on the number of input frames read from
the decoder, not on the number of frames written by the encoder. This matters
for resampling, because output frame counts can differ from input frame counts.}
@racketblock[
(audio-encode "input.flac"
"output.opus"
(hash 'bitrate 224000
'vbr? #t
'complexity 10)
#:encoder 'opus)
(audio-encode "input-96k.flac"
"output-48k.flac"
(hash 'sample-rate 48000
'bits-per-sample 24
'compression-level 8)
#:encoder 'flac)]
@section{Result hash}
The result hash contains the following keys:
@itemlist[#:style 'compact
@item{@racket['encoder], the selected backend symbol;}
@item{@racket['input] and @racket['output], the source and destination paths;}
@item{@racket['input-format], the final decoded input format hash seen by the
pipeline;}
@item{@racket['output-format], the resolved backend output format hash;}
@item{@racket['frames-read], the number of input frames consumed;}
@item{@racket['frames-written], the number of frames accepted by the backend;}
@item{@racket['tag-copy], a hash describing how metadata was handled.}]
The @racket['tag-copy] hash contains a @racket['method] key. For Opus the
method is @racket['libopusenc-comments], because metadata must be supplied to
@tt{libopusenc} before the encoder writes the OpusTags packet. For FLAC the
method is @racket['taglib-post-copy], because the encoded file is tagged after
encoding.
@section{Progress callback}
The progress callback receives a hash with at least these keys:
@itemlist[#:style 'compact
@item{@racket['phase], such as @racket['format], @racket['audio],
@racket['finished-encoding], or @racket['finished];}
@item{@racket['frames-read] and @racket['frames-written];}
@item{@racket['total-frames], when the decoder reported a known input length;}
@item{@racket['progress], a number between @racket[0.0] and @racket[1.0] when
@racket['total-frames] is known, otherwise @racket[#f];}
@item{@racket['input-format] and, after the backend has opened,
@racket['output-format].}]
A simple command-line style progress callback can print a percentage on one
line:
@racketblock[
(define (show-progress h)
(let ((p (hash-ref h 'progress #f)))
(when (number? p)
(printf "\rprogress: ~a%" (round (* 100 p)))
(flush-output))))]
@section{Opus settings}
The Opus backend uses @tt{libopusenc}. The input PCM is converted to interleaved
floating-point samples in the range @racket[-1.0] to @racket[1.0] and written
with @tt{ope_encoder_write_float}. The source sample rate is passed to
@tt{libopusenc}; @tt{libopusenc} performs the required internal resampling for
Opus output.
The following settings are recognised:
@itemlist[#:style 'compact
@item{@racket['bitrate], bitrate in bits per second. The default is
@racket[160000].}
@item{@racket['vbr?], whether variable bitrate is enabled. The default is
@racket[#t].}
@item{@racket['constrained-vbr?], whether constrained VBR is enabled. The
default is @racket[#f].}
@item{@racket['complexity], encoder complexity. The default is @racket[10].}
@item{@racket['comment-padding], Opus comment padding in bytes. The default
is @racket[512].}
@item{@racket['signal], optionally @racket['auto], @racket['voice], or
@racket['music].}
@item{@racket['lsb-depth], optionally passed to the encoder as the source
least significant bit depth.}
@item{@racket['comments], an optional hash of Opus comment strings. When
@racket[#:copy-tags?] is true, @racket[audio-encode] fills this from the
source tags.}
@item{@racket['picture], an optional picture value from @racketmodname[racket-audio/taglib].
When @racket[#:copy-tags?] is true, @racket[audio-encode] fills this
from the source tags.}]
The first backend version supports mono and stereo input.
@section{FLAC settings}
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.
The following settings are recognised:
@itemlist[#:style 'compact
@item{@racket['compression-level], FLAC compression level. The default is
@racket[5].}
@item{@racket['verify?], whether the FLAC encoder verifies encoded output. The
default is @racket[#f].}
@item{@racket['blocksize], explicit FLAC block size. The default is
@racket[0], meaning the library default.}
@item{@racket['sample-rate] or @racket['target-sample-rate], target sample rate
in Hz. Use @racket['source] or omit the key to keep the source rate.}
@item{@racket['channels] or @racket['target-channels], target channel count.
Use @racket['source] or omit the key to keep the source channel count.}
@item{@racket['bits-per-sample] or @racket['target-bits-per-sample], target
bit depth. Use @racket['source] or omit the key to keep the source bit
depth.}]
For example, a 24-bit 96 kHz FLAC file can be transcoded to 24-bit 48 kHz FLAC
with:
@racketblock[
(audio-encode "input-96k.flac"
"output-48k.flac"
(hash 'sample-rate 48000
'bits-per-sample 24
'compression-level 8)
#:encoder 'flac)]
@section{Encoder registration}
@defproc[(audio-supported-encoder-extensions) (listof string?)]{
Returns the extensions supported by the currently registered encoders. The
initial list includes @racket["flac"], @racket["opus"], and @racket["oga"].}
@defproc[(make-audio-encoder [exts (listof string?)]
[open procedure?]
[write procedure?]
[finish procedure?]
[settings procedure?])
audio-encoder?]{
Creates an encoder descriptor. The descriptor is used by
@racket[audio-register-encoder!] to register a backend.
The @racket[open] procedure receives the output file, settings hash, and input
format hash. The @racket[write] procedure receives the backend handle, buffer
format hash, byte buffer, and byte length, and returns the number of frames
accepted by the backend. The @racket[finish] procedure finalises and releases
the backend handle. The @racket[settings] procedure resolves backend defaults
against the input format and returns the output format hash.}
@defproc[(audio-encoder? [v any/c]) boolean?]{
Returns @racket[#t] when @racket[v] is an encoder descriptor.}
@defproc[(audio-register-encoder! [type symbol?]
[encoder audio-encoder?])
void?]{
Registers @racket[encoder] under @racket[type]. The encoder's extensions are
used for extension-based selection in @racket[audio-encode].}
+91
View File
@@ -0,0 +1,91 @@
#lang scribble/manual
@(require (for-label racket/base
racket/path
"../encoder-test.rkt"))
@title{Encoder Test Program}
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
@defmodule[racket-audio/encoder-test]
The @racketmodname[racket-audio/encoder-test] module is a small integration test
and command-line wrapper around @racketmodname[racket-audio/audio-encoder]. It
is useful for checking that the native encoder libraries are available and that
a concrete source file can be transcoded to Opus or FLAC.
The module depends on @filepath{tests.rkt} for its default input file. For
portable tests, pass an explicit input file.
@section{Program use}
Run the test module directly to encode the default test file to a temporary
Opus file:
@verbatim{
racket encoder-test.rkt
}
Useful command-line examples:
@verbatim{
racket encoder-test.rkt --encoder opus --input input.flac --output output.opus --bitrate-kbps 224
racket encoder-test.rkt --encoder flac --input input-96k.flac --output output-48k.flac --sample-rate 48000 --bits-per-sample 24 --compression-level 8
}
The program prints the selected encoder, settings, percentage progress, and a
summary of the result hash returned by @racket[audio-encode]. Progress is based
on input frames read from the decoder.
@section{Program options}
The command-line wrapper accepts these options:
@itemlist[#:style 'compact
@item{@tt{-e}, @tt{--encoder}: @tt{opus} or @tt{flac}.}
@item{@tt{-i}, @tt{--input}: input audio file.}
@item{@tt{-o}, @tt{--output}: output audio file.}
@item{@tt{--sample-rate}: target sample rate or @tt{source}.}
@item{@tt{--bits-per-sample}: target FLAC bit depth or @tt{source}.}
@item{@tt{--bitrate-kbps}: Opus bitrate in kbit/s.}
@item{@tt{--compression-level}: FLAC compression level.}
@item{@tt{--no-tags}: disable copying tags and embedded pictures.}]
@section{Racket functions}
@defproc[(encoder-test [input-file path-string?]
[output-file (or/c path-string? #f)]
[encoder (or/c symbol? string?)]
[settings hash?]
[#:copy-tags? copy-tags? boolean? #t])
hash?]{
Runs one encode test and prints a human-readable summary. The return value is
the result hash produced by @racket[audio-encode]. When @racket[output-file] is
@racket[#f], a temporary output path is chosen from the encoder kind.}
@defproc[(encoder-test-opus [input-file path-string?]
[output-file (or/c path-string? #f) #f]
[#:bitrate-kbps bitrate-kbps exact-positive-integer? 160]
[#:sample-rate sample-rate (or/c exact-positive-integer? 'source) 'source]
[#:copy-tags? copy-tags? boolean? #t])
hash?]{
Encodes @racket[input-file] to an Opus file using @racket[encoder-test]. The
bitrate argument is expressed in kbit/s and is converted to the @racket['bitrate]
setting used by the Opus backend.
The @racket[sample-rate] argument is normally @racket['source]. Opus encoding
passes the input rate to @tt{libopusenc}; @tt{libopusenc} performs the internal
resampling required for Opus output.}
@defproc[(encoder-test-flac [input-file path-string?]
[output-file (or/c path-string? #f) #f]
[#:compression-level compression-level exact-nonnegative-integer? 8]
[#:sample-rate sample-rate (or/c exact-positive-integer? 'source) 'source]
[#:bits-per-sample bits-per-sample (or/c exact-positive-integer? 'source) 'source]
[#:copy-tags? copy-tags? boolean? #t])
hash?]{
Encodes @racket[input-file] to a FLAC file using @racket[encoder-test]. When
@racket[sample-rate] or @racket[bits-per-sample] is not @racket['source], the
FLAC pipeline requests the corresponding output format from
@racketmodname[racket-audio/audio-encoder].}
+2
View File
@@ -21,6 +21,8 @@
@include-section["audio-player.scrbl"]
@include-section["audio-sniffer.scrbl"]
@include-section["taglib.scrbl"]
@include-section["audio-encoder.scrbl"]
@include-section["encoder-test.scrbl"]
@include-section["play-test.scrbl"]
@include-section["audio-placed-player.scrbl"]
@include-section["audio-decoder.scrbl"]