Compare commits

...

54 Commits

Author SHA1 Message Date
hans 1d9899c0d2 index 2026-08-09 19:55:28 +02:00
hans 8e6bf4e0f7 Added capability information 2026-08-08 14:20:46 +02:00
hans 070f6bd12a 'play -> 'open 2026-07-07 12:09:33 +02:00
hans 781a21a5d6 audio play! bug fix. 2026-07-07 12:07:22 +02:00
hans a4d0ae76f0 problems with values. 2026-07-07 12:02:08 +02:00
hans dc144b2f9f Dus... 2026-07-07 11:54:48 +02:00
hans 15ef81d30a error handling code/msg mixed. 2026-07-07 11:49:10 +02:00
hans 109e8ee3ad dus... 2026-07-07 11:44:05 +02:00
hans df2289e586 error debugging 2026-07-07 11:39:31 +02:00
hans b6e581aced debugging audio errors 2026-07-07 11:36:39 +02:00
hans c3eaca123e debugging errors 2026-07-07 11:29:17 +02:00
hans aa3255f104 More debugging 2026-07-07 11:11:28 +02:00
hans a9ad9bed2a error debugging 2026-07-07 10:29:25 +02:00
hans e8e0135dd8 transparent audio retval 2026-07-07 10:20:07 +02:00
hans 2271a3b7bc rpc handling 2026-07-07 10:15:30 +02:00
hans 5831273cf1 audio-errors required 2026-07-07 09:59:32 +02:00
hans 8ae86dafea Changing audio-placed-player to handle audio errors better. 2026-07-07 09:49:07 +02:00
hans 83f1d0d2ac Better error handling in audio-player.rkt 2026-07-07 09:33:50 +02:00
hans f881673b32 Audio error handling added - first basics, need to be enhanced 2026-07-07 00:44:50 +02:00
hans aecb53caf9 Somewhat more debugging info. 2026-07-06 22:07:09 +02:00
hans 819d756c04 make mutable hash for file correction. 2026-07-06 16:39:06 +02:00
hans 01752d6e4b quit patch 2026-07-06 15:20:36 +02:00
hans c8494e289e Made remote playing possible. 2026-07-06 14:28:28 +02:00
hans de443aad9d Remote audio player much simpler. 2026-07-06 11:45:10 +02:00
hans 06c332474a device bits in info hash. 2026-07-03 23:15:47 +02:00
hans e381260773 Automagic parameterization of the audio player. 2026-07-03 23:04:20 +02:00
hans 2a2616763d Index adjusted 2026-07-02 15:17:32 +02:00
hans 6f7979d046 index 2026-07-02 15:11:48 +02:00
hans 303f6957df Opus decoder is now documented 2026-07-02 15:10:30 +02:00
hans 88f911d96e Better flac error handling and reporting 2026-06-29 18:04:42 +02:00
hans b38983109b flac problems for os threads solved. 2026-06-29 15:10:15 +02:00
hans c4e1a78527 Added picture kind constants. 2026-06-25 00:01:48 +02:00
hans 2b7b02d344 typo 2026-06-24 14:31:07 +02:00
hans 492c538db1 Make sure that when only reading metadata of the flac stream, and issuing flac-stop, the flac decoder is really finished and deleted. 2026-06-24 14:30:35 +02:00
hans 9748882ee0 typo. 2026-06-24 14:08:15 +02:00
hans 66b806f59e Finish the flac decoder before deleting it. 2026-06-24 14:07:11 +02:00
hans 6197fb60e5 We want the original audio bits per sample also. 2026-06-24 13:34:24 +02:00
hans 815f39a8cc small bugs. 2026-06-10 02:30:23 +02:00
hans 0b5d8a21b4 lazy ffmpeg 2026-06-10 02:04:33 +02:00
hans 5c719ea4a0 soxr 2026-06-10 01:42:08 +02:00
hans 4f95e57a96 Load versions of libraries. 2026-06-10 00:40:14 +02:00
hans f8214b8f1b tagfile wchar fix. 2026-06-09 08:42:29 +02:00
hans 091664044c tagfile wchar fix. 2026-06-09 08:41:33 +02:00
hans 6ed566c6cd Remote usage of audio-placed-player.rkt 2026-06-08 15:46:27 +02:00
hans 17846e068c Documentation oke. 2026-06-08 13:45:54 +02:00
hans b979be540e taglib documentation. 2026-06-08 13:26:10 +02:00
hans 5eefacacba Documentation added. 2026-06-08 13:16:21 +02:00
hans 8e8b9a00c0 Conversion bug. 2026-06-08 12:17:03 +02:00
hans d6aa880104 Encoder testing 2026-06-08 12:14:32 +02:00
hans 444d62edac Audio Encoder laag. 2026-06-08 10:27:05 +02:00
hans 696ef1b978 dependency solved. 2026-06-08 09:03:31 +02:00
hans 4b6adc404e xiph opusfile support and taglib write support. 2026-06-07 23:49:38 +02:00
hans cf87fa7ed8 Merge branch 'main' of https://git.dijkewijk.nl/hans/racket-audio 2026-06-05 22:17:30 +02:00
hans d7be947886 Òpus toevoeging via xiph library 2026-06-05 22:17:10 +02:00
55 changed files with 6180 additions and 755 deletions
+162 -7
View File
@@ -1,14 +1,169 @@
# racket-audio # 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 For playback and decoding, install the native libraries used by the selected
% brew install flac backends:
% brew install mpg123
% brew install ffmpeg-full - libao
- libFLAC
- mpg123
- FFmpeg libraries, including libavutil, libavcodec and libavformat
For encoding, also install:
- libopusenc
- 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 `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.
## Debian / Ubuntu
On Debian-like systems, install the runtime and development packages for the
selected backends. A typical full setup is:
```sh
sudo apt install \
libao-dev \
libflac-dev \
libmpg123-dev \
ffmpeg \
libavutil-dev libavcodec-dev libavformat-dev libswresample-dev \
libogg-dev libopus-dev libopusenc-dev libopusfile-dev \
libsoxr-dev libtag1-dev
```
For Opus encoding, `libopusenc-dev` is required. For Opus decoding through
`opusfile-decoder.rkt`, install `libopusfile-dev` as well.
## 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 libsoxr
brew install taglib
```
Some Homebrew installations provide FFmpeg as `ffmpeg`; older local setups may
use `ffmpeg-full`.
## Windows
On Windows, the package downloader fetches the native DLL bundle from the
Codeberg `racket-sound-lib` release area. The current bundle URL pattern is:
```text
https://codeberg.org/hnmdijkema/racket-sound-lib/releases/download/1-1-2/windows-x86_64.zip
```
The archive is installed below Racket's addon directory by
`download-soundlibs`.
## 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
```
## Placed player stdio mode
The placed player can also run as a standard-port worker. In that mode the
three existing logical channels are mapped to standard streams:
```text
stdin command channel
stdout reply channel
stderr event channel
```
Because stdout and stderr are protocol streams in this mode, ordinary display
and log output is redirected. By default, `placed-player/stdio` appends such
output to a log file below Racket's standard cache directory, for example
`~/.cache/racket/racket-audio/placed-audio-player-stdio.log` on many Unix-like
systems. Pass `#:log-file #f` to discard ordinary output, or pass a path to
choose a different log file.
## Remote placed player over SSH
The placed player can also be started as a remote subprocess over SSH. In this
mode the existing three logical player channels are kept separate:
```text
stdin command channel
stdout reply channel
stderr event channel
```
The remote worker is normally started as:
```sh
racket -l racket-audio/audio-placed-player -- --stdio
```
The worker redirects ordinary logging to a cache log file so that stdout and
stderr remain serialized protocol streams. SSH, the remote Racket command and
the remote module are configured in `racket-audio.ini`. The public
`make-audio-player` API only needs the remote host and, when necessary, base
path replacements.
Example:
```racket
(define player
(make-audio-player cb-state cb-eof
#:remote-host "nas"
#:replace-base-paths
(list (cons "/muziek" "/volume1/music"))))
(audio-play! player "/muziek/klassiek/track.flac")
```
The remote host must be able to read the translated path. In the example above,
the remote worker receives `/volume1/music/klassiek/track.flac`.
+69 -20
View File
@@ -2,12 +2,15 @@
(require "flac-decoder.rkt" (require "flac-decoder.rkt"
"mp3-decoder.rkt" "mp3-decoder.rkt"
"opusfile-decoder.rkt"
"ffmpeg-decoder.rkt" "ffmpeg-decoder.rkt"
"audio-sniffer.rkt" "audio-sniffer.rkt"
"private/utils.rkt" "private/utils.rkt"
"audio-errors.rkt"
racket/contract racket/contract
racket/string racket/string
racket/path racket/path
racket-mimetypes
) )
(provide audio-open (provide audio-open
@@ -22,6 +25,10 @@
make-audio-reader make-audio-reader
audio-handle? audio-handle?
audio-supported-extensions audio-supported-extensions
audio-supported-formats
audio-decoder-for-extension
current-opusfile-output-format
opusfile-output-format?
) )
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -56,7 +63,18 @@
mp3-stop mp3-stop
'ao)) 'ao))
;; FFmpeg decodere ;; Opus, via Xiph libopusfile
(hash-set! audio-readers
'opusfile
(make-audio-reader '("opus")
opusfile-valid?
opusfile-open
opusfile-read
opusfile-seek
opusfile-stop
'ao))
;; FFmpeg decoder
(hash-set! audio-readers (hash-set! audio-readers
'ffmpeg 'ffmpeg
(make-audio-reader '("ogg" "oga" "opus" (make-audio-reader '("ogg" "oga" "opus"
@@ -92,6 +110,20 @@
(define (audio-supported-extensions) (define (audio-supported-extensions)
known-extensions) known-extensions)
(define/contract (audio-supported-formats)
(-> (listof
(cons/c string?
string?)))
(for/list ((extension
(in-list known-extensions)))
(cons
extension
(mimetype-for-ext
(string-append "audio."
extension)
#:default
"application/octet-stream"))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Register audio reader ;; Register audio reader
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -179,13 +211,20 @@
(let ((file (if (path? audio-file) (let ((file (if (path? audio-file)
(path->string audio-file) (path->string audio-file)
audio-file))) audio-file)))
(unless (audio-file-valid? audio-file)
(error (format "Not a valid audio file '~a'" audio-file)))
(unless (file-exists? audio-file) (unless (file-exists? audio-file)
(error (format "File '~a' does not exist" audio-file))) (raise-audio-error 'file-not-found
"File not found: '~a'" audio-file))
(unless (audio-file-valid? audio-file)
(raise-audio-error 'invalid-audio-file
"Not a valid audio file: '~a'" audio-file))
(let ((reader* (find-reader audio-file))) (let ((reader* (find-reader audio-file)))
(when (eq? reader* #f) (when (eq? reader* #f)
(error (format "Cannot find reader for '~a'" audio-file))) (raise-audio-error 'no-audio-reader
"Cannot find reader for '~a'" audio-file))
(let* ((reader-type (car reader*)) (let* ((reader-type (car reader*))
(reader (cadr reader*)) (reader (cadr reader*))
(ao-type (audio-reader-ao-type reader)) (ao-type (audio-reader-ao-type reader))
@@ -229,21 +268,31 @@
(not (null? (filter (λ (e) (string-ci=? ext e)) (audio-reader-exts reader))))) (not (null? (filter (λ (e) (string-ci=? ext e)) (audio-reader-exts reader)))))
(define reader-for-kind (define reader-for-kind
(make-hash '((mp3 . ffmpeg) ; ffmpeg does a better job on gapless playback... (make-hash
(flac . flac) (list (cons 'mp3 'ffmpeg) ; ffmpeg does a better job on gapless playback...
(ogg . ffmpeg) (cons 'flac 'flac)
(vorbis . ffmpeg) (cons 'ogg 'ffmpeg)
(opus . ffmpeg) (cons 'vorbis 'ffmpeg)
(wav . ffmpeg) (cons 'opus (if (opusfile-available?) 'opusfile 'ffmpeg))
(aiff . ffmpeg) (cons 'wav 'ffmpeg)
(mp4 . ffmpeg) (cons 'aiff 'ffmpeg)
(aac . ffmpeg) (cons 'mp4 'ffmpeg)
(alac . ffmpeg) (cons 'aac 'ffmpeg)
(ac3 . ffmpeg) (cons 'alac 'ffmpeg)
(ape . ffmpeg) (cons 'ac3 'ffmpeg)
(wavpack . ffmpeg) (cons 'ape 'ffmpeg)
(wma . ffmpeg) (cons 'wavpack 'ffmpeg)
(matroska . ffmpeg)))) (cons 'wma 'ffmpeg)
(cons 'matroska 'ffmpeg))))
(define/contract (audio-decoder-for-extension extension)
(-> (or/c string? symbol?)
(or/c #f symbol?))
(hash-ref
reader-for-kind
(audio-format-for-extension extension)
#f))
(define (find-reader audio-file) (define (find-reader audio-file)
+287
View File
@@ -0,0 +1,287 @@
(module audio-encoder racket/base
(require racket/path
racket/string
racket/contract
racket/runtime-path
"flac-encoder.rkt"
"opus-encoder.rkt"
"taglib.rkt"
"private/pcm-converter.rkt"
"private/utils.rkt")
(provide audio-encode
audio-supported-encoder-extensions
audio-register-encoder!
make-audio-encoder
audio-encoder?)
(define-struct audio-encoder (exts open write finish settings))
(define-runtime-module-path-index audio-decoder-module "audio-decoder.rkt")
(define audio-encoders (make-hash))
(define (audio-register-encoder! type encoder)
(hash-set! audio-encoders type encoder))
(audio-register-encoder!
'flac
(make-audio-encoder '("flac")
flac-encoder-open
flac-encoder-write
flac-encoder-finish
flac-encoder-prepare-settings))
(audio-register-encoder!
'opus
(make-audio-encoder '("opus" "oga")
opus-encoder-open
opus-encoder-write
opus-encoder-finish
opus-encoder-prepare-settings))
(define (audio-supported-encoder-extensions)
(apply append (map audio-encoder-exts (hash-values audio-encoders))))
(define (path-extension-symbol file)
(let ((ext (path-get-extension (build-path file))))
(and ext (string->symbol (string-downcase (substring (bytes->string/utf-8 ext) 1))))))
(define (encoder-for-output output-file explicit-kind)
(let ((kind (or explicit-kind (path-extension-symbol output-file))))
(cond [(and kind (hash-ref audio-encoders kind #f)) (values kind (hash-ref audio-encoders kind))]
[else (error 'audio-encode "cannot infer encoder from output file ~a" output-file)])))
(define (tag-value-copy! src dst getter setter empty?)
(let ((v (getter src)))
(unless (empty? v) (setter dst v))))
(define (empty-string? v) (or (eq? v #f) (and (string? v) (string=? v ""))))
(define (empty-number? v) (or (eq? v #f) (and (number? v) (< v 0))))
(define (merge-hash a b)
(let ((out (make-hash)))
(when (hash? a)
(for-each (lambda (k) (hash-set! out k (hash-ref a k))) (hash-keys a)))
(when (hash? b)
(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))
(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)
(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?)
(tag-value-copy! src dst tags-comment tags-comment! empty-string?)
(tag-value-copy! src dst tags-genre tags-genre! empty-string?)
(tag-value-copy! src dst tags-composer tags-composer! empty-string?)
(tag-value-copy! src dst tags-album-artist tags-album-artist! empty-string?)
(tag-value-copy! src dst tags-year tags-year! empty-number?)
(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)
(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)))
(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]
#: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) 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)
(set! frames-written (+ frames-written ((audio-encoder-write encoder) backend-handle fmt buffer buf-len))))
(define (ensure-converter! input-format)
;; FLAC may need conversion because the caller requested a target sample
;; rate or bit depth. Opus is deliberately not routed through this
;; 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 effective-settings)
(set! converter (make-pcm-converter input-format effective-settings)))))
(define (write-converted! input-format buffer buf-len)
(ensure-converter! input-format)
(cond [converter
(let-values (((out out-samples) (pcm-converter-convert converter buffer buf-len input-format)))
(when (> out-samples 0)
(write-backend! (pcm-converter-output-format converter) out (bytes-length out))))]
[else (write-backend! input-format buffer buf-len)]))
(define (drain-converter!)
(when converter
(let loop ()
(let-values (((out out-samples) (pcm-converter-drain converter)))
(when (> out-samples 0)
(write-backend! (pcm-converter-output-format converter) out (bytes-length out))
(loop))))))
(define (on-format audio-kind ao-kind handle fmt)
;; 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)
(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)
(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))
(decoder (audio-open-proc input-file on-format on-audio)))
(dynamic-wind
void
(lambda () (audio-read-proc decoder))
(lambda ()
(dynamic-wind
drain-converter!
(lambda () (when backend-handle ((audio-encoder-finish encoder) backend-handle)))
(lambda () (when converter (pcm-converter-close! converter)))))))
(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
+61
View File
@@ -0,0 +1,61 @@
#lang racket/base
(require (for-syntax racket/base)
racket/path)
(provide exn:fail:audio
exn:fail:audio?
exn:fail:audio-code
raise-audio-error
make-audio-error-code
exn-audio-code
)
(struct exn:fail:audio exn:fail
(code source line)
#:transparent)
(define exn-audio-code exn:fail:audio-code)
(define (audio-codes)
'(no-decoder no-encoder
file-not-found
invalid-audio-file
no-audio-reader
audio-file-corrupt
audio-error))
(define (make-audio-code code)
(if (symbol? code)
(letrec ((f (λ (l)
(if (null? (cdr l))
(car l)
(if (eq? (car l) code)
code
(f (cdr l)))))))
(f (audio-codes)))
(make-audio-code (string->symbol (format "~a" code)))))
(define (make-audio-error-code c)
(make-audio-code c))
(define (raise-audio-error* code source line message . args)
(let* ((msg (apply format (cons message args)))
(src* (file-name-from-path (build-path source)))
(msg* (format "~a at ~a, line ~a" msg src* line)))
(raise (exn:fail:audio msg*
(current-continuation-marks)
(make-audio-code code)
source
line))))
(define-syntax (raise-audio-error stx)
(syntax-case stx ()
((_ code message ...)
(let ((src (syntax-source stx))
(line (syntax-line stx)))
#`(raise-audio-error* code '#,src '#,line message ...)))))
+120 -29
View File
@@ -1,27 +1,53 @@
#lang racket/base #lang racket/base
(require racket/place (require racket/port
racket/async-channel port-channel
uni-channel
"libao.rkt" "libao.rkt"
"audio-decoder.rkt" "audio-decoder.rkt"
"audio-errors.rkt"
"private/utils.rkt" "private/utils.rkt"
early-return early-return
) )
(provide placed-player (provide placed-player
placed-player/stdio
audio-known-exts? audio-known-exts?
) )
(define get-current-seconds current-seconds) (define get-current-seconds current-seconds)
(define (placed-player/stdio #:log-file [log-file (racket-sound-log-file 'placed-audio-player-stdio)])
(define stdin-ch
(make-uni-channel
(make-port-channel (current-input-port) #:direction 'input #:source 'stdin #:close? #f)))
(define stdout-ch
(make-uni-channel
(make-port-channel (current-output-port) #:direction 'output #:source 'stdout #:close? #f)))
(define stderr-ch
(make-uni-channel
(make-port-channel (current-error-port) #:direction 'output #:source 'stderr #:close? #f)))
(define log-port (if log-file (open-racket-sound-log-file log-file) (open-output-nowhere)))
(dynamic-wind
void
(lambda ()
;; stdout and stderr are protocol channels in this mode. Redirect ordinary
;; output so that display/log output cannot corrupt the serialized channel
;; streams.
(parameterize ([current-output-port log-port]
[current-error-port log-port])
(placed-player stdin-ch stdout-ch stderr-ch)))
(lambda ()
(with-handlers ([exn:fail? void]) (close-output-port log-port)))))
(define (eq-seconds? s1 s2) (define (eq-seconds? s1 s2)
(let ((s1* (inexact->exact (round s1))) (let ((s1* (inexact->exact (round s1)))
(s2* (inexact->exact (round s2)))) (s2* (inexact->exact (round s2))))
(= s1* s2*))) (= s1* s2*)))
(define (placed-player ch-in) (define (placed-player ch-in [initial-ch-out #f] [initial-ch-evt #f])
(let ((ch-evt #f) (let ((ch-evt initial-ch-evt)
(ch-out #f) (ch-out initial-ch-out)
(ao-h #f) (ao-h #f)
(ao-mutex (make-mutex)) (ao-mutex (make-mutex))
(ao-dec #f) (ao-dec #f)
@@ -53,20 +79,30 @@
(begin b1 ...) (begin b1 ...)
r))))) r)))))
(define (->uni-channel ch)
(if (uni-channel? ch) ch (make-uni-channel ch)))
;; ch-in is supplied by dynamic-place or by the async/thread launcher.
;; ch-out and ch-evt are supplied by the init command. Each logical
;; channel is wrapped as a uni-channel on the side where it is used; raw
;; place channels must not be wrapped before they are sent through init.
(set! ch-in (->uni-channel ch-in))
(when ch-out (set! ch-out (->uni-channel ch-out)))
(when ch-evt (set! ch-evt (->uni-channel ch-evt)))
(define (put data) (define (put data)
(if (place-channel? ch-out) (uni-channel-put ch-out data))
(place-channel-put ch-out data)
(async-channel-put ch-out data)))
(define (evt data) (define (evt data)
(if (place-channel? ch-evt) (uni-channel-put ch-evt data))
(place-channel-put ch-evt data)
(async-channel-put ch-evt data)))
(define (get) (define (get)
(if (place-channel? ch-in) (uni-channel-get ch-in))
(place-channel-get ch-in)
(async-channel-get ch-in))) (define (close-and-wait ch)
(when ch
(uni-channel-close ch)
(uni-channel-wait ch)))
(define (audio-read-worker ao-dec file-id) (define (audio-read-worker ao-dec file-id)
(set! feeding-audio #t) (set! feeding-audio #t)
@@ -78,7 +114,10 @@
(set! feeding-audio #f) (set! feeding-audio #f)
(set! feed-interrupted #f) (set! feed-interrupted #f)
(set! player-state 'stopped) (set! player-state 'stopped)
(evt (list 'exception (exn-message e))))]) (let ((code (if (exn:fail:audio? e) (exn:fail:audio-code e) 'no-audio-error)))
(state (format "audio-read-worker: exception ~a" code) evt)
(evt (list 'exception (exn-message e) code)))
)])
(dynamic-wind (dynamic-wind
void void
(λ () (λ ()
@@ -337,6 +376,32 @@
(audio-read-worker ao-dec current-file-id) (audio-read-worker ao-dec current-file-id)
current-file-id) current-file-id)
(define (param! par val)
(cond
((eq? par 'opus-bits)
(if (integer? val)
(cond
((= val 16)
(current-opusfile-output-format 's16)
16
)
((= val 24)
(current-opusfile-output-format 's24)
24)
(else 'error-unsupported-value)
)
'error-wrong-value-type)
)
(else 'error-unknown-param)))
(define (param par)
(cond
((eq? par 'opus-bits)
(if (eq? (current-opusfile-output-format) 's16)
16
24))
(else 'error-unknown-param)))
(define (pause paused) (define (pause paused)
(when (or (eq? player-state 'paused) (when (or (eq? player-state 'paused)
(eq? player-state 'playing)) (eq? player-state 'playing))
@@ -349,7 +414,8 @@
(audio-seek ao-dec percentage))) (audio-seek ao-dec percentage)))
(define (volume percentage) (define (volume percentage)
(set! req-volume percentage)) (set! req-volume percentage)
(check-volume))
(define (ao-buf-ms) (define (ao-buf-ms)
(ao-playback-buf-ms)) (ao-playback-buf-ms))
@@ -371,6 +437,7 @@
(if (null? r) #f (cdar r)))) (if (null? r) #f (cdar r))))
(hash-set! h 'state player-state) (hash-set! h 'state player-state)
(hash-set! h 'valid-ao-handle (ao-valid? ao-h)) (hash-set! h 'valid-ao-handle (ao-valid? ao-h))
(hash-set! h 'ao-device-bits (if (ao-valid? ao-h) (ao-device-bits ao-h) #f))
(hash-set! h 'duration (if (ao-valid? ao-h) (ao-music-duration ao-h) #f)) (hash-set! h 'duration (if (ao-valid? ao-h) (ao-music-duration ao-h) #f))
(hash-set! h 'at-second (if (ao-valid? ao-h) (ao-at-second ao-h) #f)) (hash-set! h 'at-second (if (ao-valid? ao-h) (ao-at-second ao-h) #f))
(hash-set! h 'at-music-id m-id) (hash-set! h 'at-music-id m-id)
@@ -413,25 +480,29 @@
(with-handlers ([exn:fail? (λ (e) (with-handlers ([exn:fail? (λ (e)
(if (eq? ch-evt #f) (if (eq? ch-evt #f)
(raise e) (raise e)
(begin (let ((code (if (exn:fail:audio? e) (exn:fail:audio-code e) 'no-audio-error)))
(evt (list 'exception (evt (list 'exception (exn-message e) code))
(exn-message e)))
(when in-rpc (when in-rpc
(put (list 'error (put (list 'error (exn-message e) code))
(exn-message e)))
(set! in-rpc #f)) (set! in-rpc #f))
(loop)) (loop))
))]) ))])
(cond (cond
((eq? cmd 'quit) (do-rpc ((eq? cmd 'quit)
(stop-and-cleanup) ;; Quit is the one RPC where the acknowledgement must be written
(set! player-state 'quit) ;; before the worker starts tearing down its audio state and stdio
(state "quit" evt 'force) ;; transport. For port-based uni-channels, put is asynchronous:
'(quit))) ;; close-and-wait makes sure the queued '(quit) has actually been
;; written and flushed before the process exits.
(put '(quit))
(close-and-wait ch-out)
(stop-and-cleanup)
(set! player-state 'quit)
(close-and-wait ch-evt))
((eq? cmd 'init) (do-rpc ((eq? cmd 'init) (do-rpc
(set! ch-out (cadr data)) (set! ch-out (->uni-channel (cadr data)))
(set! ch-evt (caddr data)) (set! ch-evt (->uni-channel (caddr data)))
'(initialized)) '(initialized))
(loop)) (loop))
(else (else
@@ -478,6 +549,15 @@
(do-rpc (do-rpc
(stop-and-cleanup) (stop-and-cleanup)
'(ok))) '(ok)))
((eq? cmd 'param!)
(do-rpc
(let ((par (cadr data))
(value (caddr data)))
(list (param! par value)))))
((eq? cmd 'param)
(do-rpc
(let ((par (cadr data)))
(list (param par)))))
((eq? cmd 'state) ((eq? cmd 'state)
(do-rpc (do-rpc
(let ((st #f)) (let ((st #f))
@@ -491,7 +571,7 @@
)) ))
(else (else
(do-rpc (do-rpc
(list 'error (format "Unknown command ~a" cmd)))) (list 'error (format "Unknown command ~a" cmd) 'error-unknown-command)))
) )
(loop) (loop)
) )
@@ -502,3 +582,14 @@
) )
) )
) )
(module+ main
(require racket/cmdline)
(define log-file 'default)
(command-line
#:once-each
[("--stdio") "Run the placed audio player over stdin/stdout/stderr." (void)]
[("--log-file") file "Write ordinary worker logging to file." (set! log-file file)]
[("--no-log-file") "Discard ordinary worker logging." (set! log-file #f)])
(placed-player/stdio #:log-file (if (eq? log-file 'default)
(racket-sound-log-file 'placed-audio-player-stdio)
log-file)))
+165 -57
View File
@@ -4,8 +4,13 @@
racket/contract racket/contract
racket/async-channel racket/async-channel
racket/runtime-path racket/runtime-path
racket/string
"audio-errors.rkt"
uni-channel
"audio-placed-player.rkt" "audio-placed-player.rkt"
"opusfile-decoder.rkt"
"private/utils.rkt" "private/utils.rkt"
"private/remote-utils.rkt"
(prefix-in ffi: ffi/unsafe) (prefix-in ffi: ffi/unsafe)
) )
@@ -33,12 +38,17 @@
audio-ao-buf-ms! audio-ao-buf-ms!
audio-ao-buf-ms audio-ao-buf-ms
audio-known-exts? audio-known-exts?
audio-parameterize!
audio-param!
audio-param
replace-base-path
) )
(define-runtime-path placed-player-module "audio-placed-player.rkt") (define-runtime-path placed-player-module "audio-placed-player.rkt")
(define-struct audio-play (define-struct audio-play
(valid? cb-state cb-eof-stream rpc au-place evt-thread state) (valid? cb-state cb-eof-stream rpc au-place evt-thread state replace-base-paths)
#:mutable #:mutable
#:transparent #:transparent
) )
@@ -52,24 +62,39 @@
(define (percentage? p) (define (percentage? p)
(and (number? p) (>= p 0))) (and (number? p) (>= p 0)))
(define (any? x)
#t)
(define (max-percentage? n) (define (max-percentage? n)
(λ (p) (and (percentage? p) (λ (p) (and (percentage? p)
(<= p n)))) (<= p n))))
(define (is-return? retval sym) (define (is-return? retval sym)
;(displayln retval)
(if (list? retval) (if (list? retval)
(if (null? retval) (if (null? retval)
#f #f
(eq? (car retval) sym)) (eq? (car retval) sym))
#f)) #f))
(define-struct internal-audio-retval
(kind code info retval)
#:transparent
)
(define (to-ret-value ret) (define (to-ret-value ret)
(if (list? ret) (if (list? ret)
(if (null? ret) (if (null? ret)
(error (format "audio-player: no return value in ~a" ret)) (raise-audio-error 'audio-error "audio-player: no return value, empty list returned")
(car ret)) (let ((r (car ret)))
ret)) (if (eq? r 'error)
(let ((msg (cadr ret))
(code (caddr ret)))
(err-sound "Got an error: ~a - ~a" code msg)
(make-internal-audio-retval 'error code msg 'error))
(make-internal-audio-retval 'retval #f #f r))))
(if (eq? ret 'error)
(make-internal-audio-retval 'error 'audio-error "An undefined error was raised in the audio-player" 'error)
(make-internal-audio-retval 'retval #f #f ret))))
(define (is-event? evt sym) (define (is-event? evt sym)
(is-return? evt sym)) (is-return? evt sym))
@@ -77,14 +102,37 @@
(define (evt-data evt) (define (evt-data evt)
(cadr evt)) (cadr evt))
(define (correct-to-os-path h replacements)
(let ((file (hash-ref h 'file #f)))
(letrec ((f (λ (l fl)
(if (null? l)
fl
(let ((bp-remote (cdar l))
(bp-local (caar l)))
(f (cdr l) (string-replace fl bp-remote bp-local)))))))
(unless (eq? file #f)
(let* ((delim (if (eq? (system-type 'os) 'windows) "\\" "/"))
(file* (string-replace
(string-replace
(f replacements (format "~a" file)) "/" delim)
"\\" delim)))
(hash-set! h 'file (build-path file*))))
h)))
(define-syntax assert (define-syntax assert
(syntax-rules () (syntax-rules ()
((_ cond message ...) ((_ cond message ...)
(unless cond (error (format message ...)))))) (unless cond (error (format message ...))))))
(define/contract (make-audio-player cb-state cb-eof-stream (define/contract (make-audio-player cb-state cb-eof-stream
#:use-place [use-place (place-enabled?)]) #:use-place [use-place (place-enabled?)]
(->* (procedure? procedure?) (#:use-place boolean?) audio-play?) #:remote-host [remote-host #f]
#:replace-base-paths [replace-base-paths '()])
(->* (procedure? procedure?)
(#:use-place boolean?
#:remote-host (or/c #f string?)
#:replace-base-paths replace-base-paths?)
audio-play?)
(let ((cmd-ch #f) (let ((cmd-ch #f)
(ret-ch #f) (ret-ch #f)
(evt-ch #f) (evt-ch #f)
@@ -96,38 +144,64 @@
(rpc #f) (rpc #f)
(rpc-mutex (make-mutex)) (rpc-mutex (make-mutex))
) )
(if use-place (cond
(begin [remote-host
(set! cmd-ch (dynamic-place placed-player-module 'placed-player)) ;; Remote mode is deliberately narrow: make-audio-player only needs
(set! cmd-put (λ (data) (place-channel-put cmd-ch data))) ;; a host and optional base-path replacements. SSH, Racket and module
(set! au-pl cmd-ch) ;; details are encapsulated in private/remote-utils.rkt and its config.
(set! dead-guard (λ () (let ((evt (place-dead-evt au-pl))) (let-values (((cmd-ch* ret-ch* evt-ch* proc dead-guard*)
(sync evt)))) (start-remote-placed-player remote-host)))
(let-values (((ret-ch-in ret-ch-out) (place-channel)) (set! cmd-ch cmd-ch*)
((evt-ch-in evt-ch-out) (place-channel))) (set! ret-ch ret-ch*)
(place-channel-put cmd-ch (list 'init ret-ch-out evt-ch-out)) (set! evt-ch evt-ch*)
(set! evt-ch evt-ch-in) (set! au-pl proc)
(set! ret-ch ret-ch-in) (set! dead-guard dead-guard*))]
(assert (is-return? (place-channel-get ret-ch-in) 'initialized) [use-place
"Unexpected: not 'initialized returnd from 'init command")) ;; dynamic-place returns the command place-channel. The raw channel
) ;; is kept for place-dead-evt, while normal traffic is sent through
(begin ;; a uni-channel wrapper.
(set! cmd-ch (make-async-channel)) (let ((raw-cmd-ch (dynamic-place placed-player-module 'placed-player)))
(set! cmd-put (λ (data) (async-channel-put cmd-ch data))) (set! cmd-ch (make-uni-channel raw-cmd-ch))
(set! au-pl (thread (λ () (placed-player cmd-ch)))) (set! au-pl raw-cmd-ch)
(set! dead-guard (λ () (let ((evt (thread-dead-evt au-pl))) (set! dead-guard (lambda () (let ((evt (place-dead-evt au-pl)))
(sync evt)))) (sync evt))))
(set! ret-ch (make-async-channel)) (let-values (((ret-ch-in ret-ch-out) (place-channel))
(set! evt-ch (make-async-channel)) ((evt-ch-in evt-ch-out) (place-channel)))
(async-channel-put cmd-ch (list 'init ret-ch evt-ch)) ;; Do not send uni-channel structs through a place-channel: they
(assert (is-return? (async-channel-get ret-ch) 'initialized) ;; contain procedures and are not place-message values. Send the
"Unexpected: not 'initialized returnd from 'init command") ;; raw channels and let the worker wrap them on its own side.
) (set! ret-ch (make-uni-channel ret-ch-in))
) (set! evt-ch (make-uni-channel evt-ch-in))
(set! ret-get (λ () (to-ret-value (sync ret-ch)))) (uni-channel-put cmd-ch (list 'init ret-ch-out evt-ch-out))
(set! evt-get (λ (timeout-ms) (sync/timeout (/ timeout-ms 1000) evt-ch))) (assert (is-return? (uni-channel-get ret-ch) 'initialized)
(set! rpc (λ (cmd . args) (with-mutex rpc-mutex "Unexpected: not 'initialized returned from 'init command")))]
(cmd-put (cons cmd args)) (ret-get)))) [else
(let ((raw-cmd-ch (make-async-channel)))
(set! cmd-ch (make-uni-channel raw-cmd-ch))
(set! au-pl (thread (lambda () (placed-player raw-cmd-ch))))
(set! dead-guard (lambda () (let ((evt (thread-dead-evt au-pl)))
(sync evt))))
(let ((raw-ret-ch (make-async-channel))
(raw-evt-ch (make-async-channel)))
;; As in place mode, pass raw channels during init and keep
;; uni-channel wrappers on each side for all subsequent traffic.
(set! ret-ch (make-uni-channel raw-ret-ch))
(set! evt-ch (make-uni-channel raw-evt-ch))
(uni-channel-put cmd-ch (list 'init raw-ret-ch raw-evt-ch))
(assert (is-return? (uni-channel-get ret-ch) 'initialized)
"Unexpected: not 'initialized returned from 'init command")))])
(set! cmd-put (λ (data) (uni-channel-put cmd-ch data)))
(set! ret-get (λ () (to-ret-value (uni-channel-get ret-ch))))
(set! evt-get (λ (timeout-ms) (sync/timeout (/ timeout-ms 1000)
(uni-channel-get-evt evt-ch))))
(set! rpc (lambda (cmd . args)
(with-mutex rpc-mutex
(define args*
(if (and (eq? cmd 'open) (pair? args))
(cons (replace-base-path (car args) replace-base-paths) (cdr args))
args))
(cmd-put (cons cmd args*))
(ret-get))))
(let* ((handle #f) (let* ((handle #f)
(cb-state* (λ (st st-hash) (cb-state handle st st-hash))) (cb-state* (λ (st st-hash) (cb-state handle st st-hash)))
@@ -137,7 +211,8 @@
rpc rpc
au-pl au-pl
#f #f
(make-hash))) (make-hash)
replace-base-paths))
(set-audio-play-evt-thread! handle (set-audio-play-evt-thread! handle
(thread (thread
(λ () (λ ()
@@ -146,9 +221,11 @@
(let ((e (evt-get 500))) (let ((e (evt-get 500)))
(cond ((eq? e #f) (void)) (cond ((eq? e #f) (void))
((is-event? e 'state) ((is-event? e 'state)
(let ((data (evt-data e))) (let* ((data (evt-data e))
(set-audio-play-state! handle (car data)) (h (hash-copy (car data))))
(cb-state* (cadr data) (car data)))) (correct-to-os-path h replace-base-paths)
(set-audio-play-state! handle h)
(cb-state* (cadr data) h)))
((is-event? e 'audio-done) (cb-eof*)) ((is-event? e 'audio-done) (cb-eof*))
((is-event? e 'exception) ((is-event? e 'exception)
(err-sound "audio-player: exception event: ~a" e)) (err-sound "audio-player: exception event: ~a" e))
@@ -170,6 +247,7 @@
(when (hash? (audio-play-state handle)) (when (hash? (audio-play-state handle))
(let ((h (hash-copy (audio-play-state handle)))) (let ((h (hash-copy (audio-play-state handle))))
(hash-set! h 'state 'invalid) (hash-set! h 'state 'invalid)
(correct-to-os-path h replace-base-paths)
(set-audio-play-state! handle h))) (set-audio-play-state! handle h)))
(dbg-sound "audio-play handle invalidated and cleaned of references") (dbg-sound "audio-play handle invalidated and cleaned of references")
)) ))
@@ -179,46 +257,57 @@
(when (audio-play? h) (when (audio-play? h)
(rpc 'quit)))) (rpc 'quit))))
(audio-parameterize! handle)
handle) handle)
) )
) )
(define-syntax ap-rpc
(syntax-rules ()
((_ handle cmd args ...)
(let ((rv ((audio-play-rpc handle) cmd args ...)))
(if (eq? (internal-audio-retval-kind rv) 'error)
(raise-audio-error (internal-audio-retval-code rv) (internal-audio-retval-info rv))
(internal-audio-retval-retval rv))))
)
)
(define/contract (audio-play! handle audio-file) (define/contract (audio-play! handle audio-file)
(-> audio-play? path-string? number?) (-> audio-play? path-string? number?)
(let ((result ((audio-play-rpc handle) 'open audio-file))) (let ((r (ap-rpc handle 'open audio-file)))
(when (eq? result 'error) (if (eq? (car r) 'ok)
(error "Got an error from the placed audio player")) (cadr r)
(cadr result))) (raise-audio-error 'audio-error "audio-play!: unexpected return value from 'open command: ~a" r))))
(define/contract (audio-pause! handle paused) (define/contract (audio-pause! handle paused)
(-> audio-play? boolean? symbol?) (-> audio-play? boolean? symbol?)
((audio-play-rpc handle) 'pause paused)) (ap-rpc handle 'pause paused))
(define/contract (audio-paused? handle) (define/contract (audio-paused? handle)
(-> audio-play? boolean?) (-> audio-play? boolean?)
((audio-play-rpc handle) 'paused)) (ap-rpc handle 'paused))
(define/contract (audio-stop! handle) (define/contract (audio-stop! handle)
(-> audio-play? symbol?) (-> audio-play? symbol?)
((audio-play-rpc handle) 'stop)) (ap-rpc handle 'stop))
(define/contract (audio-quit! handle) (define/contract (audio-quit! handle)
(-> audio-play? (or/c number? boolean? symbol?)) (-> audio-play? (or/c number? boolean? symbol?))
(let ((r ((audio-play-rpc handle) 'quit))) (let ((r (ap-rpc handle 'quit)))
(set-audio-play-valid?! handle #f) (set-audio-play-valid?! handle #f)
r)) r))
(define/contract (audio-seek! handle percentage) (define/contract (audio-seek! handle percentage)
(-> audio-play? (max-percentage? 100) symbol?) (-> audio-play? (max-percentage? 100) symbol?)
((audio-play-rpc handle) 'seek percentage)) (ap-rpc handle 'seek percentage))
(define/contract (audio-volume! handle percentage) (define/contract (audio-volume! handle percentage)
(-> audio-play? percentage? symbol?) (-> audio-play? percentage? symbol?)
((audio-play-rpc handle) 'volume percentage)) (ap-rpc handle 'volume percentage))
(define/contract (audio-volume handle) (define/contract (audio-volume handle)
(-> audio-play? percentage?) (-> audio-play? percentage?)
((audio-play-rpc handle) 'get-volume)) (ap-rpc handle 'get-volume))
(define/contract (audio-full-state handle) (define/contract (audio-full-state handle)
(-> audio-play? hash?) (-> audio-play? hash?)
@@ -271,15 +360,34 @@
(-> audio-play? number? number? (or/c symbol? boolean?)) (-> audio-play? number? number? (or/c symbol? boolean?))
(let ((from (if (< min 1) 1 (if (> min 10) 10 min))) (let ((from (if (< min 1) 1 (if (> min 10) 10 min)))
(until (if (< max min) (+ min 1) (if (> max 30) 30 max)))) (until (if (< max min) (+ min 1) (if (> max 30) 30 max))))
((audio-play-rpc handle) 'buf-seconds from until))) (ap-rpc handle 'buf-seconds from until)))
(define/contract (audio-ao-buf-ms! handle ms) (define/contract (audio-ao-buf-ms! handle ms)
(-> audio-play? integer? (or/c integer? boolean?)) (-> audio-play? integer? (or/c integer? boolean?))
((audio-play-rpc handle) 'ao-buf-ms ms)) (ap-rpc handle 'ao-buf-ms ms))
(define/contract (audio-ao-buf-ms handle) (define/contract (audio-ao-buf-ms handle)
(-> audio-play? (or/c integer? boolean?)) (-> audio-play? (or/c integer? boolean?))
((audio-play-rpc handle) 'ao-buf-ms)) (ap-rpc handle 'ao-buf-ms))
(define/contract (audio-param! handle param value)
(-> audio-play? symbol? any? any?)
(ap-rpc handle 'param! param value))
(define/contract (audio-param handle param)
(-> audio-play? symbol? any?)
(ap-rpc handle 'param param))
(define/contract (audio-parameterize! handle)
(-> audio-play? void?)
(void
(let* ((opus-fmt (current-opusfile-output-format))
(opus-bits (if (eq? opus-fmt 's24) 24 16))
)
(audio-param! handle 'opus-bits opus-bits)
)
)
)
+32 -16
View File
@@ -10,6 +10,7 @@
audio-sniff-format audio-sniff-format
audio-sniff-format/extension audio-sniff-format/extension
audio-sniff-extension audio-sniff-extension
audio-format-for-extension
audio-format-matches? audio-format-matches?
audio-format-known?) audio-format-known?)
@@ -46,6 +47,30 @@
(substring s 1) (substring s 1)
s))]))) s))])))
(define (audio-format-for-extension* extension)
(let* ((text (string-downcase
(format "~a" extension)))
(normalized
(if (string-prefix? text ".")
(substring text 1)
text)))
(case (string->symbol normalized)
[(mp3 mp2 mp1) 'mp3]
[(flac) 'flac]
[(ogg oga) 'ogg]
[(opus) 'opus]
[(wav wave) 'wav]
[(aif aiff aifc) 'aiff]
[(m4a mp4 m4b m4p) 'mp4]
[(aac) 'aac]
[(alac) 'alac]
[(ac3) 'ac3]
[(ape) 'ape]
[(wv wvp wvpk wavpack) 'wavpack]
[(wma asf) 'wma]
[(webm mka mkv) 'matroska]
[else 'unknown])))
(define (file-readable-status file) (define (file-readable-status file)
(cond (cond
[(not (file-exists? file)) 'file-not-found] [(not (file-exists? file)) 'file-not-found]
@@ -294,22 +319,9 @@
fmt] fmt]
[(not (eq? fmt 'unknown)) fmt] [(not (eq? fmt 'unknown)) fmt]
[else [else
(case (string->symbol (or (audio-sniff-extension* file) "")) (audio-format-for-extension*
[(mp3 mp2 mp1) 'mp3] (or (audio-sniff-extension* file)
[(flac) 'flac] ""))])))
[(ogg oga) 'ogg]
[(opus) 'opus]
[(wav wave) 'wav]
[(aif aiff aifc) 'aiff]
[(m4a mp4 m4b m4p) 'mp4]
[(aac) 'aac]
[(alac) 'alac]
[(ac3) 'ac3]
[(ape) 'ape]
[(wv wvp wvpk wavpack) 'wavpack]
[(wma asf) 'wma]
[(webm mka mkv) 'matroska]
[else 'unknown])])))
(define (audio-format-known?* fmt) (define (audio-format-known?* fmt)
(not (eq? (memq fmt audio-formats) #f))) (not (eq? (memq fmt audio-formats) #f)))
@@ -321,6 +333,10 @@
(-> path-string? (or/c string? #f)) (-> path-string? (or/c string? #f))
(audio-sniff-extension* file)) (audio-sniff-extension* file))
(define/contract (audio-format-for-extension extension)
(-> (or/c string? symbol?) audio-format?)
(audio-format-for-extension* extension))
(define/contract (audio-sniff-format file) (define/contract (audio-sniff-format file)
(-> path-string? audio-format?) (-> path-string? audio-format?)
(audio-sniff-format* file)) (audio-sniff-format* file))
+187
View File
@@ -0,0 +1,187 @@
#lang racket/base
(require "audio-encoder.rkt"
"tests.rkt"
simple-log
racket/cmdline
racket/file
racket/path
racket/string)
(provide encoder-test
encoder-test-opus
encoder-test-flac)
(define (setting-value v)
(cond ((or (eq? v #f) (eq? v 'source)) 'source)
((string? v)
(let ((s (string-downcase v)))
(if (string=? s "source")
'source
(let ((n (string->number v)))
(if n n (raise-argument-error 'encoder-test "number or source" v))))))
(else v)))
(define (encoder-symbol v)
(cond ((symbol? v) v)
((string? v) (string->symbol (string-downcase v)))
(else (raise-argument-error 'encoder-test "encoder name" v))))
(define (default-output-file encoder)
(build-path (find-system-path 'temp-dir)
(format "racket-audio-encoder-test.~a"
(case encoder
((opus) "opus")
((flac) "flac")
(else (raise-argument-error 'encoder-test "opus or flac" encoder))))))
(define (opus-settings bitrate-kbps sample-rate)
(if (eq? sample-rate 'source)
(hash 'bitrate (* bitrate-kbps 1000)
'vbr? #t
'complexity 10)
(hash 'bitrate (* bitrate-kbps 1000)
'vbr? #t
'complexity 10
'sample-rate sample-rate)))
(define (flac-settings compression-level sample-rate bits-per-sample)
(let ((h (make-hash)))
(hash-set! h 'compression-level compression-level)
(unless (eq? sample-rate 'source) (hash-set! h 'sample-rate sample-rate))
(unless (eq? bits-per-sample 'source) (hash-set! h 'bits-per-sample bits-per-sample))
h))
(define (format-summary fmt)
(if (hash? fmt)
(format "rate=~a, channels=~a, bits=~a, frames=~a"
(hash-ref fmt 'sample-rate "?")
(hash-ref fmt 'channels "?")
(hash-ref fmt 'bits-per-sample "?")
(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)
(displayln "")
(displayln "Encoder result")
(displayln "--------------")
(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))))
(when (file-exists? out) (delete-file out))
(displayln (format "Encoding ~a" input-file))
(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?
#:progress-callback (make-progress-callback)))))
(define (encoder-test-opus [input-file test-file3]
[output-file #f]
#:bitrate-kbps [bitrate-kbps 160]
#:sample-rate [sample-rate 'source]
#:copy-tags? [copy-tags? #t])
(encoder-test input-file output-file 'opus
(opus-settings bitrate-kbps (setting-value sample-rate))
#:copy-tags? copy-tags?))
(define (encoder-test-flac [input-file test-file3]
[output-file #f]
#:compression-level [compression-level 8]
#:sample-rate [sample-rate 'source]
#:bits-per-sample [bits-per-sample 'source]
#:copy-tags? [copy-tags? #t])
(encoder-test input-file output-file 'flac
(flac-settings compression-level
(setting-value sample-rate)
(setting-value bits-per-sample))
#:copy-tags? copy-tags?))
(module+ main
(sl-log-to-display)
(define encoder 'opus)
(define input-file test-file3)
(define output-file #f)
(define copy-tags? #t)
(define bitrate-kbps 160)
(define compression-level 8)
(define sample-rate 'source)
(define bits-per-sample 'source)
(command-line
#:program "encoder-test.rkt"
#:once-each
(("-e" "--encoder") e "Encoder: opus or flac. Default: opus."
(set! encoder (encoder-symbol e)))
(("-i" "--input") f "Input audio file. Default: tests.rkt test-file3."
(set! input-file f))
(("-o" "--output") f "Output audio file. Default: temp test file."
(set! output-file f))
(("--sample-rate") r "Target sample rate, e.g. 48000, or source. Default: source."
(set! sample-rate (setting-value r)))
(("--bits-per-sample") b "Target FLAC bits per sample, e.g. 16/24, or source. Default: source."
(set! bits-per-sample (setting-value b)))
(("--bitrate-kbps") b "Opus bitrate in kbps. Default: 160."
(set! bitrate-kbps (or (string->number b)
(raise-argument-error 'encoder-test "number" b))))
(("--compression-level") n "FLAC compression level. Default: 8."
(set! compression-level (or (string->number n)
(raise-argument-error 'encoder-test "number" n))))
(("--no-tags") "Do not copy tags/pictures to the output file."
(set! copy-tags? #f))
#:args rest
(cond ((null? rest) (void))
((null? (cdr rest)) (set! input-file (car rest)))
((null? (cddr rest)) (set! input-file (car rest)) (set! output-file (cadr rest)))
(else (raise-user-error 'encoder-test "too many positional arguments: ~a" rest))))
(case encoder
((opus)
(encoder-test-opus input-file output-file
#:bitrate-kbps bitrate-kbps
#:sample-rate sample-rate
#:copy-tags? copy-tags?))
((flac)
(encoder-test-flac input-file output-file
#:compression-level compression-level
#:sample-rate sample-rate
#:bits-per-sample bits-per-sample
#:copy-tags? copy-tags?))
(else (raise-argument-error 'encoder-test "opus or flac" encoder))))
+180 -49
View File
@@ -29,7 +29,20 @@
fmpg-buffer-start-sample fmpg-buffer-start-sample
fmpg-buffer-end-sample fmpg-buffer-end-sample
fmpg-sample-position fmpg-sample-position
ffmpeg-version) ffmpeg-version
;; Shared FFmpeg/swresample bindings for encoder-side PCM conversion.
;; Keeping these exports here prevents a second, divergent FFmpeg FFI
;; version layer in private/pcm-converter.rkt.
AV_SAMPLE_FMT_S32
swr_alloc_set_opts2
swr_init
swr_free
swr_get_out_samples
swr_get_delay
swr_convert
ffmpeg-make-default-channel-layout
ffmpeg-channel-layout-uninit!)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; C - Types & Functions ;; C - Types & Functions
@@ -64,22 +77,26 @@
;; FFmpeg DLL names often contain the major version; on Unix-like systems ;; FFmpeg DLL names often contain the major version; on Unix-like systems
;; the dynamic linker can usually find the generic soname. ;; the dynamic linker can usually find the generic soname.
(define libavutil (get-lib (case (system-type 'os) (define libavutil (get-lib/quiet (case (system-type 'os)
[(windows) '("avutil-60")] [(windows) '("avutil-60")]
[else '("avutil" "libavutil")]) '(#f))) [else '("avutil" "libavutil")])
(ffmpeg-lib-versions 'avutil)))
(define libswresample (get-lib (case (system-type 'os) (define libswresample (get-lib/quiet (case (system-type 'os)
[(windows) '("swresample-6")] [(windows) '("swresample-6")]
[else '("swresample" "libswresample")]) '(#f))) [else '("swresample" "libswresample")])
(ffmpeg-lib-versions 'swresample)))
(define libavcodec (get-lib (case (system-type 'os) (define libavcodec (get-lib/quiet (case (system-type 'os)
[(windows) '("avcodec-62")] [(windows) '("avcodec-62")]
[else '("avcodec" "libavcodec")]) '(#f))) [else '("avcodec" "libavcodec")])
(ffmpeg-lib-versions 'avcodec)))
(define libavformat (get-lib (case (system-type 'os) (define libavformat (get-lib/quiet (case (system-type 'os)
[(windows) '("avformat-62")] [(windows) '("avformat-62")]
[else '("avformat" "libavformat")]) '(#f))) [else '("avformat" "libavformat")])
(ffmpeg-lib-versions 'avformat)))
(define-ffi-definer def-avutil libavutil #:default-make-fail make-not-available) (define-ffi-definer def-avutil libavutil #:default-make-fail make-not-available)
@@ -92,10 +109,47 @@
(def-avformat avformat_version (_fun -> _uint)) (def-avformat avformat_version (_fun -> _uint))
(def-swresample swresample_version (_fun -> _uint)) (def-swresample swresample_version (_fun -> _uint))
(define avutil-version-major (quotient (avutil_version) 65536)) ;; Version functions are regular FFmpeg symbols, but they must not be called
(define avcodec-version-major (quotient (avcodec_version) 65536)) ;; unconditionally at module-load time. When one of the native FFmpeg
(define avformat-version-major (quotient (avformat_version) 65536)) ;; libraries is missing, define-ffi-definer creates a make-not-available
(define swresample-version-major (quotient (swresample_version) 65536)) ;; procedure. Calling that procedure here would make (require ...) fail even
;; for callers that do not use FFmpeg. Keep the runtime version as #f when the
;; library is unavailable and use the highest supported major only to choose a
;; harmless struct layout for the dormant bindings.
(define (packed-version->list v)
(list (quotient v 65536) (remainder (quotient v 256) 256) (remainder v 256)))
(define (ffmpeg-default-major lib)
(cadr (hash-ref valid-ffmpeg-versions lib)))
(define (ffmpeg-version-packed/false lib version-proc)
(with-handlers ([exn:fail? (λ (e) #f)])
(and lib (version-proc))))
(define avutil-version-packed (ffmpeg-version-packed/false libavutil avutil_version))
(define avcodec-version-packed (ffmpeg-version-packed/false libavcodec avcodec_version))
(define avformat-version-packed (ffmpeg-version-packed/false libavformat avformat_version))
(define swresample-version-packed (ffmpeg-version-packed/false libswresample swresample_version))
(define (ffmpeg-packed-version lib)
(cond ((eq? lib 'avutil) avutil-version-packed)
((eq? lib 'avcodec) avcodec-version-packed)
((eq? lib 'avformat) avformat-version-packed)
((or (eq? lib 'swr)
(eq? lib 'swresample)) swresample-version-packed)
(else (error (format "Unknown library '~a" lib)))))
(define (ffmpeg-runtime-major lib)
(let ((v (ffmpeg-packed-version lib)))
(and v (car (packed-version->list v)))))
(define (ffmpeg-layout-major lib)
(or (ffmpeg-runtime-major lib) (ffmpeg-default-major lib)))
(define avutil-version-major (ffmpeg-layout-major 'avutil))
(define avcodec-version-major (ffmpeg-layout-major 'avcodec))
(define avformat-version-major (ffmpeg-layout-major 'avformat))
(define swresample-version-major (ffmpeg-layout-major 'swresample))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Version check ;; Version check
@@ -106,16 +160,16 @@
;; so the rest of the module can choose version-dependent layouts. ;; so the rest of the module can choose version-dependent layouts.
(define (ffmpeg-version lib) (define (ffmpeg-version lib)
;; FFmpeg packs versions as major<<16 | minor<<8 | micro. ;; FFmpeg packs versions as major<<16 | minor<<8 | micro.
(let ((v (λ (v) (list (quotient v 65536) (remainder (quotient v 256) 256) (remainder v 256))))) ;; If the requested library is missing, call its make-not-available stub so
(cond ((eq? lib 'avutil) (v (avutil_version))) ;; callers get the same error style as every other unavailable FFI binding.
((eq? lib 'avcodec) (v (avcodec_version))) (let ((v (ffmpeg-packed-version lib)))
((eq? lib 'avformat) (v (avformat_version))) (cond (v (packed-version->list v))
((eq? lib 'avutil) (packed-version->list (avutil_version)))
((eq? lib 'avcodec) (packed-version->list (avcodec_version)))
((eq? lib 'avformat) (packed-version->list (avformat_version)))
((or (eq? lib 'swr) ((or (eq? lib 'swr)
(eq? lib 'swresample)) (v (swresample_version))) (eq? lib 'swresample)) (packed-version->list (swresample_version)))
(else (error (format "Unknown library '~a" lib))) (else (error (format "Unknown library '~a" lib))))))
)
)
)
;; Formats the runtime version of an FFmpeg library as text. ;; Formats the runtime version of an FFmpeg library as text.
;; This is mainly used in error messages and logging. ;; This is mainly used in error messages and logging.
@@ -124,31 +178,64 @@
;; Support ffmpeg 6, 7 and 8 ;; Support ffmpeg 6, 7 and 8
;; Checks at load time whether the detected FFmpeg major versions are supported. ;; Logs at load time whether the detected FFmpeg major versions are supported.
;; The struct layouts below are deliberately partial and major-version-dependent; ;; The struct layouts below are deliberately partial and major-version-dependent,
;; therefore an unknown major version must fail early and loudly. ;; so actual FFmpeg entry points call ensure-ffmpeg-library! before using them.
(define (ffmpeg-raise-unavailable! lib)
;; Deliberately call the library's version binding. When the native library
;; is missing this is the make-not-available stub installed by
;; define-ffi-definer, so the normal FFI unavailable-library message is kept.
(cond ((eq? lib 'avutil) (avutil_version))
((eq? lib 'avcodec) (avcodec_version))
((eq? lib 'avformat) (avformat_version))
((or (eq? lib 'swr)
(eq? lib 'swresample)) (swresample_version))
(else (error (format "Unknown library '~a" lib)))))
(define (ensure-ffmpeg-library! lib)
;; Keep require-time lazy: missing and unsupported libraries are only errors
;; at the boundary where FFmpeg functionality is actually used.
(let ((v (ffmpeg-packed-version lib)))
(cond
((not v) (ffmpeg-raise-unavailable! lib))
(else
(let* ((major-version (car (packed-version->list v)))
(from (car (hash-ref valid-ffmpeg-versions lib)))
(until (cadr (hash-ref valid-ffmpeg-versions lib))))
(when (or (< major-version from) (> major-version until))
(error
(format "Unsupported major version of ffmpeg library ~a: ~a (~a).
Supported range: ~a - ~a"
lib major-version (ffmpeg-version-string lib) from until)))))))
#t)
(define (ensure-ffmpeg-libraries! libs)
(for-each ensure-ffmpeg-library! libs)
#t)
(define (ensure-ffmpeg-decoder!)
;; The FFmpeg decoder path uses demuxing, decoding, avutil helpers and
;; swresample for conversion to the internal S32 PCM output.
(ensure-ffmpeg-libraries! '(avutil avcodec avformat swresample)))
(define-syntax check-support (define-syntax check-support
(syntax-rules () (syntax-rules ()
((_ lib version-hash) ((_ lib version-hash)
(let ((from (car (hash-ref version-hash lib))) (let ((from (car (hash-ref version-hash lib)))
(until (cadr (hash-ref version-hash lib)))) (until (cadr (hash-ref version-hash lib))))
;; Only the major version determines whether the C struct layouts below are safe. ;; Probe only for logging. Do not fail at require-time: even unsupported
(let ((major-version (car (ffmpeg-version lib)))) ;; versions should not break users that do not call FFmpeg functionality.
(let ((major-version (ffmpeg-runtime-major lib)))
(cond (cond
((not major-version)
(warn-sound "FFmpeg library ~a is not available; FFmpeg bindings stay dormant until the library is installed" lib))
((or (< major-version from) (> major-version until)) ((or (< major-version from) (> major-version until))
(error (warn-sound "Unsupported ffmpeg library ~a - version ~a; FFmpeg calls will fail until a supported version is installed"
(format "Unsupported major version of ffmpeg library ~a: ~a (~a).\nSupported range: ~a - ~a" lib (ffmpeg-version-string lib)))
'lib major-version (ffmpeg-version-string lib) from until)))
(else (else
(info-sound "Supported ffmpeg library ~a - version ~a between ~a and ~a" (info-sound "Supported ffmpeg library ~a - version ~a between ~a and ~a"
lib (ffmpeg-version-string lib) from until) lib (ffmpeg-version-string lib) from until))))))))
)
)
)
)
)
)
)
(check-support 'avutil valid-ffmpeg-versions) (check-support 'avutil valid-ffmpeg-versions)
(check-support 'avcodec valid-ffmpeg-versions) (check-support 'avcodec valid-ffmpeg-versions)
@@ -248,6 +335,26 @@
) )
) )
(def-avutil av_channel_layout_default
(_fun _AVChannelLayout-pointer _int -> _void))
(def-avutil av_channel_layout_uninit
(_fun _AVChannelLayout-pointer -> _void))
(define (ffmpeg-make-default-channel-layout channels)
(ensure-ffmpeg-library! 'avutil)
(let ((p (cast (malloc (ctype-sizeof _AVChannelLayout) 'atomic-interior)
_pointer
_AVChannelLayout-pointer)))
(av_channel_layout_default p channels)
p))
(define (ffmpeg-channel-layout-uninit! p)
(when p
(ensure-ffmpeg-library! 'avutil)
(av_channel_layout_uninit p))
#t)
; _AVCodecParameters: ; _AVCodecParameters:
; codec_type : AVMediaType ; codec_type : AVMediaType
; codec_id : int / AVCodecID ; codec_id : int / AVCodecID
@@ -583,7 +690,10 @@
;; This prevents decoder-storage from keeping an old native pointer after cleanup. ;; This prevents decoder-storage from keeping an old native pointer after cleanup.
(define (swr_free ctx) (define (swr_free ctx)
(if ctx (if ctx
(begin (swr_free/raw ctx) #f) (begin
(ensure-ffmpeg-library! 'swresample)
(swr_free/raw ctx)
#f)
#f)) #f))
(def-avformat avformat_close_input/raw (_fun (_ptr io _AVFormatContext-pointer/null) (def-avformat avformat_close_input/raw (_fun (_ptr io _AVFormatContext-pointer/null)
@@ -641,7 +751,7 @@
(begin (av_frame_free/raw frm) #f) (begin (av_frame_free/raw frm) #f)
#f)) #f))
(def-swresample swr_alloc_set_opts2 (def-swresample swr_alloc_set_opts2/raw
(_fun (ps : (_ptr io _SwrContext)) (_fun (ps : (_ptr io _SwrContext))
_AVChannelLayout-pointer ; out_ch_layout _AVChannelLayout-pointer ; out_ch_layout
_AVSampleFormat ; out_sample_fmt _AVSampleFormat ; out_sample_fmt
@@ -652,10 +762,18 @@
_int ; log_offset _int ; log_offset
_pointer ; log_ctx _pointer ; log_ctx
-> (r : _int) -> (r : _int)
-> (values r ps))) -> (values r ps)) #:c-id swr_alloc_set_opts2)
(def-swresample swr_init (define (swr_alloc_set_opts2 ps out-ch-layout out-sample-fmt out-sample-rate in-ch-layout in-sample-fmt in-sample-rate log-offset log-ctx)
(_fun _SwrContext -> _int)) (ensure-ffmpeg-library! 'swresample)
(swr_alloc_set_opts2/raw ps out-ch-layout out-sample-fmt out-sample-rate in-ch-layout in-sample-fmt in-sample-rate log-offset log-ctx))
(def-swresample swr_init/raw
(_fun _SwrContext -> _int) #:c-id swr_init)
(define (swr_init ctx)
(ensure-ffmpeg-library! 'swresample)
(swr_init/raw ctx))
(def-avcodec avcodec_parameters_alloc (def-avcodec avcodec_parameters_alloc
(_fun -> _AVCodecParameters-pointer/null)) (_fun -> _AVCodecParameters-pointer/null))
@@ -694,14 +812,26 @@
(_fun _AVFrame-pointer -> _int64)) (_fun _AVFrame-pointer -> _int64))
(def-swresample swr_get_out_samples (def-swresample swr_get_out_samples/raw
(_fun _SwrContext _int -> _int)) (_fun _SwrContext _int -> _int) #:c-id swr_get_out_samples)
(def-swresample swr_convert (define (swr_get_out_samples ctx in-samples)
(_fun _SwrContext _pointer _int _pointer _int -> _int)) (ensure-ffmpeg-library! 'swresample)
(swr_get_out_samples/raw ctx in-samples))
(def-swresample swr_get_delay (def-swresample swr_convert/raw
(_fun _SwrContext _int64 -> _int64)) (_fun _SwrContext _pointer _int _pointer _int -> _int) #:c-id swr_convert)
(define (swr_convert ctx out out-count in in-count)
(ensure-ffmpeg-library! 'swresample)
(swr_convert/raw ctx out out-count in in-count))
(def-swresample swr_get_delay/raw
(_fun _SwrContext _int64 -> _int64) #:c-id swr_get_delay)
(define (swr_get_delay ctx base)
(ensure-ffmpeg-library! 'swresample)
(swr_get_delay/raw ctx base))
(def-avutil av_samples_get_buffer_size (def-avutil av_samples_get_buffer_size
(_fun _pointer _int _int _AVSampleFormat _int -> _int)) (_fun _pointer _int _int _AVSampleFormat _int -> _int))
@@ -1226,6 +1356,7 @@
;; Opens an audio file and initializes all decode state. ;; Opens an audio file and initializes all decode state.
;; The function fails safely with 0: at every failed step, the half-open instance is closed. ;; The function fails safely with 0: at every failed step, the half-open instance is closed.
(define (fmpg-open-file! instance filename) (define (fmpg-open-file! instance filename)
(ensure-ffmpeg-decoder!)
;; First check the API preconditions: valid instance, not already open, ;; First check the API preconditions: valid instance, not already open,
;; no old format context, and a filename that FFmpeg can open. ;; no old format context, and a filename that FFmpeg can open.
(let/assert (let/assert
+35 -20
View File
@@ -15,8 +15,6 @@
flac-stop flac-stop
flac-seek flac-seek
(all-from-out "flac-definitions.rkt") (all-from-out "flac-definitions.rkt")
kinds
last-buffer last-buf-len
) )
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -42,24 +40,13 @@
(define (flac-stream-state handle) (define (flac-stream-state handle)
((flac-handle-ffi-decoder-handler handle) 'state)) ((flac-handle-ffi-decoder-handler handle) 'state))
(define (process-frame handle h mem-out)
(define kinds (make-hash))
(define last-buffer #f)
(define last-buf-len #f)
(define (process-frame handle h mem-out)
(let* ([cb-audio (flac-handle-cb-audio handle)] (let* ([cb-audio (flac-handle-cb-audio handle)]
[type (hash-ref h 'number-type)] [type (hash-ref h 'number-type)]
[buf-size (bytes-length mem-out)]) [buf-size (bytes-length mem-out)])
(hash-set! h 'duration (flac-duration handle)) (hash-set! h 'duration (flac-duration handle))
(set! last-buffer mem-out)
(set! last-buf-len buf-size)
(hash-set! kinds type #t)
(when (procedure? cb-audio) (when (procedure? cb-audio)
(cb-audio h mem-out buf-size)) (cb-audio h mem-out buf-size))
@@ -83,6 +70,7 @@
(hash-ref mh 'sample-rate))))) (hash-ref mh 'sample-rate)))))
(hash-set! mh 'duration duration)) (hash-set! mh 'duration duration))
(set-flac-handle-stream-info! handle si) (set-flac-handle-stream-info! handle si)
(hash-set! mh 'audio-bits-per-sample (hash-ref mh 'bits-per-sample))
(hash-set! mh 'bits-per-sample 32) ; Flac works internally 32 bits. (hash-set! mh 'bits-per-sample 32) ; Flac works internally 32 bits.
(let ((cb (flac-handle-cb-stream-info handle))) (let ((cb (flac-handle-cb-stream-info handle)))
(when (procedure? cb) (when (procedure? cb)
@@ -91,9 +79,24 @@
) )
) )
(define (flac-err->str nr)
(cond
((= nr 0) "0 = Lost Sync: An error in the stream caused the decoder to lose synchronization.")
((= nr 1) "1 = Bad Header: The decoder encountered a corrupted frame header.")
((= nr 2) "2 = CRC Mismatch: The frame's data did not match the CRC in the footer.")
((= nr 3) "3 = Unparseable Stream: The decoder encountered reserved fields in use in the stream.")
((= nr 4) "4 = Bad Metadata: The decoder encountered a corrupted metadata block.")
((= nr 5) "5 = Out of bounds: The decoder encountered a otherwise valid frame in which the decoded samples exceeded the range offered by the stated bit depth.")
((= nr 6) "6 = Missing Frame: Two adjacent frames had frame numbers increasing by more than 1 or sample numbers increasing by more than the blocksize, indicating that one or more frame/frames was missing between them.")
(else (format "~a = Unknown: Unknown status code." nr))
)
)
(define (flac-read handle) (define (flac-read handle)
(let* ((ffi-handler (flac-handle-ffi-decoder-handler handle)) (let* ((ffi-handler (flac-handle-ffi-decoder-handler handle))
(state (ffi-handler 'state))) (state (ffi-handler 'state))
(err-hash (make-hash))
)
(set-flac-handle-stop-reading! handle #f) (set-flac-handle-stop-reading! handle #f)
(set-flac-handle-reading! handle #t) (set-flac-handle-reading! handle #t)
(letrec ((reader (lambda (frame-nr) (letrec ((reader (lambda (frame-nr)
@@ -110,7 +113,12 @@
st frame-nr (ffi-handler 'int-state)) st frame-nr (ffi-handler 'int-state))
) )
(when (ffi-handler 'has-errno?) (when (ffi-handler 'has-errno?)
(err-sound "Error in stream: ~a" (ffi-handler 'errno)) (let ((errno (ffi-handler 'errno)))
(unless (eq? (hash-ref err-hash errno #f) #f)
(hash-set! err-hash errno #t)
(warn-sound "Error in stream: ~a (frame-nr = ~a) (int-state = ~a)"
(flac-err->str errno)
frame-nr (ffi-handler 'int-state))))
) )
(when (ffi-handler 'has-meta-data?) (when (ffi-handler 'has-meta-data?)
(ffi-handler 'process-meta-data (ffi-handler 'process-meta-data
@@ -170,14 +178,21 @@
(define (flac-stop handle) (define (flac-stop handle)
(let ((ct (current-milliseconds))) (let ((ct (current-milliseconds)))
(dbg-sound "requesting stop at: ~a" ct) (dbg-sound "requesting stop at: ~a" ct)
(set-flac-handle-stop-reading! handle #t) (if (flac-handle-reading handle)
(while (flac-handle-reading handle) (begin ; in reading
(sleep 0.01)) (set-flac-handle-stop-reading! handle #t)
(while (flac-handle-reading handle)
(sleep 0.01)))
(begin ; never starte reading, or reading finished
(dbg-sound "not reading flac, need to finish flac")
(let ((ffi-handler (flac-handle-ffi-decoder-handler handle)))
(unless (eq? ffi-handler #f)
(ffi-handler 'delete))))
)
(let ((ct* (current-milliseconds))) (let ((ct* (current-milliseconds)))
(dbg-sound "stop came back at: ~a" ct*) (dbg-sound "stop came back at: ~a" ct*)
(dbg-sound "flac-stop took: ~a ms" (- ct* ct))) (dbg-sound "flac-stop took: ~a ms" (- ct* ct)))
) )
) )
); end of module ); end of module
+15
View File
@@ -25,6 +25,13 @@
flac-bits-per-sample flac-bits-per-sample
flac-total-samples flac-total-samples
flac-duration flac-duration
flac-encoder-handle
make-flac-encoder-handle
flac-encoder-handle-ffi-encoder-handler
flac-encoder-handle-settings
flac-encoder-handle-format
flac-encoder-handle-file
) )
(define-struct flac-stream-info (define-struct flac-stream-info
@@ -105,4 +112,12 @@
;#:transparent ;#:transparent
) )
;; A high level FLAC encoder handle. The actual native encoder pointer
;; remains encapsulated in the FFI command handler, matching the existing
;; decoder-side style in this package.
(define-struct flac-encoder-handle
(ffi-encoder-handler settings format file)
#:transparent)
); end of module ); end of module
+90
View File
@@ -0,0 +1,90 @@
(module flac-encoder racket/base
(require "libflac-ffi.rkt"
"flac-definitions.rkt")
(provide flac-encoder-available?
flac-encoder-default-settings
flac-encoder-prepare-settings
flac-encoder-open
flac-encoder-write
flac-encoder-finish)
(define (flac-encoder-available?) #t)
(define (copy-hash h)
(let ((out (make-hash)))
(for-each (lambda (k) (hash-set! out k (hash-ref h k))) (hash-keys h))
out))
(define (hash-ref/default h k default)
(if (hash-has-key? h k) (hash-ref h k) default))
(define (hash-merge base override)
(let ((out (copy-hash base)))
(when (hash? override)
(for-each (lambda (k) (hash-set! out k (hash-ref override k))) (hash-keys override)))
out))
(define (flac-encoder-default-settings)
(make-hash '((compression-level . 5)
(verify? . #f)
(blocksize . 0))))
(define (source-value v source)
(if (eq? v 'source) source v))
(define (safe-flac-bits bits)
(cond [(and (integer? bits) (or (= bits 8) (= bits 12) (= bits 16) (= bits 20) (= bits 24))) bits]
[(and (integer? bits) (< bits 16)) 16]
[else 24]))
(define (flac-encoder-prepare-settings settings format)
(let* ((base (flac-encoder-default-settings))
(h (hash-merge base settings))
;; In encoder settings, 'sample-rate means the requested output rate.
;; 'target-sample-rate is accepted as an explicit alias for readability.
(source-rate (hash-ref format 'sample-rate))
(source-channels (hash-ref format 'channels))
(source-bits (hash-ref/default format 'bits-per-sample 24))
(rate (source-value (hash-ref/default h 'target-sample-rate
(hash-ref/default h 'sample-rate source-rate))
source-rate))
(channels (source-value (hash-ref/default h 'target-channels
(hash-ref/default h 'channels source-channels))
source-channels))
(bits0 (source-value (hash-ref/default h 'target-bits-per-sample
(hash-ref/default h 'bits-per-sample source-bits))
source-bits))
(bits (safe-flac-bits bits0))
(total (hash-ref/default h 'total-samples (hash-ref/default format 'total-samples #f))))
(hash-set! h 'sample-rate rate)
(hash-set! h 'channels channels)
(hash-set! h 'bits-per-sample bits)
(when (hash-has-key? h 'target-sample-rate) (hash-remove! h 'target-sample-rate))
(when (hash-has-key? h 'target-channels) (hash-remove! h 'target-channels))
(when (hash-has-key? h 'target-bits-per-sample) (hash-remove! h 'target-bits-per-sample))
(when (and total (integer? total) (>= total 0)) (hash-set! h 'total-samples total))
(unless (hash-has-key? h 'streamable-subset?) (hash-set! h 'streamable-subset? (<= bits 24)))
h))
(define (flac-encoder-open output-file settings format)
(let* ((file (if (path? output-file) (path->string output-file) output-file))
(resolved (flac-encoder-prepare-settings settings format))
(handler (flac-ffi-encoder-handler)))
(handler 'new)
(handler 'configure resolved)
(handler 'init file)
(make-flac-encoder-handle handler resolved format file)))
(define (flac-encoder-write handle buf-info buffer buf-len)
((flac-encoder-handle-ffi-encoder-handler handle) 'write buffer buf-len buf-info))
(define (flac-encoder-finish handle)
(let ((handler (flac-encoder-handle-ffi-encoder-handler handle)))
(dynamic-wind
void
(lambda () (handler 'finish))
(lambda () (handler 'delete)))))
) ; end of module
+6 -2
View File
@@ -14,8 +14,12 @@
(define deps (define deps
'("racket/gui" "racket/base" "racket" '("racket/gui" "racket/base" "racket"
"finalizer" "draw-lib" "net-lib" "finalizer" "draw-lib" "net-lib"
"simple-log" "racket-sprintf" "simple-log" "simple-ini" "racket-sprintf"
"early-return" "let-assert") "racket-mimetypes"
"early-return" "let-assert"
"uni-channel" "port-channel"
"rackunit-lib"
)
) )
(define build-deps (define build-deps
+5 -3
View File
@@ -64,10 +64,12 @@
(define libao (define libao
(get-lib (list (case (system-type 'os) (get-lib (list (case (system-type 'os)
[(windows) "libao-1.2.2"] [(windows) "libao-1.2.2"]
[else "libao"])) '(#f))) [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-device (_cpointer/null 'ao-device))
(define _ao-option (_cpointer/null 'ao-option)) (define _ao-option (_cpointer/null 'ao-option))
+253 -35
View File
@@ -6,6 +6,7 @@
) )
(provide flac-ffi-decoder-handler (provide flac-ffi-decoder-handler
flac-ffi-encoder-handler
_FLAC__StreamMetadata _FLAC__StreamMetadata
FLAC__StreamMetadata-type FLAC__StreamMetadata-type
flac-ffi-meta flac-ffi-meta
@@ -15,7 +16,7 @@
) )
(define lib (get-lib '("libFLAC") '(#f))) (define lib (get-lib '("FLAC" "libFLAC") (linux-lib-versions '("14" "12" "8" #f))))
(define-ffi-definer define-libflac lib (define-ffi-definer define-libflac lib
#:default-make-fail make-not-available) #:default-make-fail make-not-available)
@@ -371,6 +372,7 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define _FLAC__StreamDecoder-pointer (_cpointer 'flac-streamdecoder)) (define _FLAC__StreamDecoder-pointer (_cpointer 'flac-streamdecoder))
(define _FLAC__StreamEncoder-pointer (_cpointer 'flac-streamencoder))
(define _FLAC__Data-pointer (_cpointer/null 'flac-client-data)) (define _FLAC__Data-pointer (_cpointer/null 'flac-client-data))
;(define _FLAC__StreamMetadata-pointer (_cpointer/null 'flac-stream-metadata)) ;(define _FLAC__StreamMetadata-pointer (_cpointer/null 'flac-stream-metadata))
@@ -378,27 +380,46 @@
;; FLAC Callback function definitions ;; FLAC Callback function definitions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; libFLAC keeps the C callback pointers that are passed to
;; FLAC__stream_decoder_init_file and calls them later from
;; FLAC__stream_decoder_process_single. When process_single runs in a
;; Racket thread with #:pool 'own, those callbacks can arrive from a
;; different OS thread than the ordinary Racket coroutine-thread OS
;; thread. Use #:async-apply so Racket can transfer such callback
;; invocations safely, and use a per-decoder #:keep box so the generated
;; callback stubs stay reachable as long as the decoder can call them.
(define (flac-callback-async-apply thunk)
(thunk))
;typedef FLAC__StreamDecoderWriteStatus(* FLAC__StreamDecoderWriteCallback) (const FLAC__StreamDecoder *decoder, const FLAC__Frame *frame, const FLAC__int32 *const buffer[], void *client_data) ;typedef FLAC__StreamDecoderWriteStatus(* FLAC__StreamDecoderWriteCallback) (const FLAC__StreamDecoder *decoder, const FLAC__Frame *frame, const FLAC__int32 *const buffer[], void *client_data)
(define _FLAC__StreamDecoderWriteCallback (define (make-FLAC__StreamDecoderWriteCallback keep-box)
(_fun _FLAC__StreamDecoder-pointer (_cprocedure
_FLAC__Frame-pointer (list _FLAC__StreamDecoder-pointer
FLAC__int32** _FLAC__Frame-pointer
_FLAC__Data-pointer FLAC__int32**
-> _int)) _FLAC__Data-pointer)
_int
#:async-apply flac-callback-async-apply
#:keep keep-box))
;typedef void(* FLAC__StreamDecoderMetadataCallback) (const FLAC__StreamDecoder *decoder, const FLAC__StreamMetadata *metadata, void *client_data) ;typedef void(* FLAC__StreamDecoderMetadataCallback) (const FLAC__StreamDecoder *decoder, const FLAC__StreamMetadata *metadata, void *client_data)
(define _FLAC__StreamDecoderMetadataCallback (define (make-FLAC__StreamDecoderMetadataCallback keep-box)
(_fun _FLAC__StreamDecoder-pointer (_cprocedure
_FLAC__StreamMetadata-pointer (list _FLAC__StreamDecoder-pointer
_FLAC__Data-pointer _FLAC__StreamMetadata-pointer
-> _void)) _FLAC__Data-pointer)
_void
#:async-apply flac-callback-async-apply
#:keep keep-box))
(define _FLAC__StreamDecoderErrorCallback (define (make-FLAC__StreamDecoderErrorCallback keep-box)
(_fun _FLAC__StreamDecoder-pointer (_cprocedure
_int (list _FLAC__StreamDecoder-pointer
_FLAC__Data-pointer _int
-> _void)) _FLAC__Data-pointer)
_void
#:async-apply flac-callback-async-apply
#:keep keep-box))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -411,6 +432,9 @@
(define-libflac FLAC__stream_decoder_delete (define-libflac FLAC__stream_decoder_delete
(_fun _FLAC__StreamDecoder-pointer -> _void)) (_fun _FLAC__StreamDecoder-pointer -> _void))
(define-libflac FLAC__stream_decoder_finish
(_fun _FLAC__StreamDecoder-pointer -> _int))
(define-libflac FLAC__stream_decoder_get_state (define-libflac FLAC__stream_decoder_get_state
(_fun _FLAC__StreamDecoder-pointer -> _int)) (_fun _FLAC__StreamDecoder-pointer -> _int))
@@ -434,9 +458,9 @@
(define-libflac FLAC__stream_decoder_init_file (define-libflac FLAC__stream_decoder_init_file
(_fun _FLAC__StreamDecoder-pointer (_fun _FLAC__StreamDecoder-pointer
_string/utf-8 _string/utf-8
_FLAC__StreamDecoderWriteCallback _pointer
_FLAC__StreamDecoderMetadataCallback _pointer
_FLAC__StreamDecoderErrorCallback _pointer
_FLAC__Data-pointer ; Seen by Jens Axel Søgaard - Is already present in FLAC 1.4.3 _FLAC__Data-pointer ; Seen by Jens Axel Søgaard - Is already present in FLAC 1.4.3
-> _int)) -> _int))
@@ -519,26 +543,46 @@
(define fl #f) (define fl #f)
(define flac-file #f) (define flac-file #f)
(define client-data #f) (define client-data #f)
(define callback-keepers (box null))
(define (make-decoder-callback proc type)
;; Convert the Racket procedure to a C function pointer with a type
;; that uses callback-keepers as #:keep. The raw pointer is passed to
;; libFLAC, while the keeper box retains the generated callback stub
;; until delete releases it after FLAC__stream_decoder_finish/delete.
(cast proc type _pointer))
;(define (write-callback fl frame buffer client-data) ;(define (write-callback fl frame buffer client-data)
; (set! write-data (append write-data (list (cons frame buffer)))) ; (set! write-data (append write-data (list (cons frame buffer))))
; 0) ; 0)
(define (write-callback fl frame buffer client-data) (define (write-callback fl frame buffer client-data)
(set! write-data (cons (copy-flac-frame frame buffer) write-data)) (with-handlers ([exn:fail?
0) (lambda (e)
;; Never let a Racket exception escape through a C
;; callback. Return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT.
(set! error-no -2)
1)])
(set! write-data (cons (copy-flac-frame frame buffer) write-data))
0))
;(define (meta-callback fl meta client-data) ;(define (meta-callback fl meta client-data)
; (let ((meta-clone (FLAC__metadata_object_clone meta))) ; (let ((meta-clone (FLAC__metadata_object_clone meta)))
; (unless (eq? meta-clone #f) ; (unless (eq? meta-clone #f)
; (set! meta-data (append meta-data (list meta-clone)))))) ; (set! meta-data (append meta-data (list meta-clone))))))
(define (meta-callback fl meta client-data) (define (meta-callback fl meta client-data)
(let ((meta-clone (FLAC__metadata_object_clone meta))) (with-handlers ([exn:fail?
(unless (eq? meta-clone #f) (lambda (e)
(set! meta-data (cons meta-clone meta-data))))) ;; Metadata callbacks return void; remember failure and
;; let the decoder state/error path report it.
(set! error-no -3)
(void))])
(let ((meta-clone (FLAC__metadata_object_clone meta)))
(unless (eq? meta-clone #f)
(set! meta-data (cons meta-clone meta-data))))))
(define (error-callback fl errno client-data) (define (error-callback fl errno client-data)
(set! error-no errno) (set! error-no errno)
) (void))
(define (new) (define (new)
(dbg-sound "flac-ffi 'new") (dbg-sound "flac-ffi 'new")
@@ -549,13 +593,25 @@
(define (init file) (define (init file)
(dbg-sound "flac-ffi 'init") (dbg-sound "flac-ffi 'init")
(let ((r (FLAC__stream_decoder_init_file (let* ((write-callback-ptr
fl (make-decoder-callback
file write-callback
write-callback (make-FLAC__StreamDecoderWriteCallback callback-keepers)))
meta-callback (meta-callback-ptr
error-callback (make-decoder-callback
client-data))) meta-callback
(make-FLAC__StreamDecoderMetadataCallback callback-keepers)))
(error-callback-ptr
(make-decoder-callback
error-callback
(make-FLAC__StreamDecoderErrorCallback callback-keepers)))
(r (FLAC__stream_decoder_init_file
fl
file
write-callback-ptr
meta-callback-ptr
error-callback-ptr
client-data)))
(set! flac-file file) (set! flac-file file)
r)) r))
@@ -564,9 +620,12 @@
(if (eq? fl #f) (if (eq? fl #f)
(error "flac handler has already been deleted") (error "flac handler has already been deleted")
(begin (begin
(FLAC__stream_decoder_finish fl)
(FLAC__stream_decoder_delete fl) (FLAC__stream_decoder_delete fl)
(set! fl #f))) (set! fl #f)
) ;; libFLAC cannot call the callbacks anymore after finish/delete,
;; so the generated callback stubs can now be released.
(set-box! callback-keepers null))))
(define (process-single) (define (process-single)
(FLAC__stream_decoder_process_single fl)) (FLAC__stream_decoder_process_single fl))
@@ -639,5 +698,164 @@
)) ))
) )
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Direct FLAC encoder interface
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define FLAC__StreamEncoderInitStatus-ok 0)
(define _FLAC__StreamEncoderProgressCallback _pointer)
(define-libflac FLAC__stream_encoder_new
(_fun -> _FLAC__StreamEncoder-pointer))
(define-libflac FLAC__stream_encoder_delete
(_fun _FLAC__StreamEncoder-pointer -> _void))
(define-libflac FLAC__stream_encoder_finish
(_fun _FLAC__StreamEncoder-pointer -> FLAC__bool))
(define-libflac FLAC__stream_encoder_get_state
(_fun _FLAC__StreamEncoder-pointer -> _int))
(define-libflac FLAC__stream_encoder_set_verify
(_fun _FLAC__StreamEncoder-pointer FLAC__bool -> FLAC__bool))
(define-libflac FLAC__stream_encoder_set_streamable_subset
(_fun _FLAC__StreamEncoder-pointer FLAC__bool -> FLAC__bool))
(define-libflac FLAC__stream_encoder_set_channels
(_fun _FLAC__StreamEncoder-pointer _uint32_t -> FLAC__bool))
(define-libflac FLAC__stream_encoder_set_bits_per_sample
(_fun _FLAC__StreamEncoder-pointer _uint32_t -> FLAC__bool))
(define-libflac FLAC__stream_encoder_set_sample_rate
(_fun _FLAC__StreamEncoder-pointer _uint32_t -> FLAC__bool))
(define-libflac FLAC__stream_encoder_set_compression_level
(_fun _FLAC__StreamEncoder-pointer _uint32_t -> FLAC__bool))
(define-libflac FLAC__stream_encoder_set_blocksize
(_fun _FLAC__StreamEncoder-pointer _uint32_t -> FLAC__bool))
(define-libflac FLAC__stream_encoder_set_total_samples_estimate
(_fun _FLAC__StreamEncoder-pointer FLAC__uint64 -> FLAC__bool))
(define-libflac FLAC__stream_encoder_init_file
(_fun _FLAC__StreamEncoder-pointer
_string/utf-8
_FLAC__StreamEncoderProgressCallback
_FLAC__Data-pointer
-> _int))
(define-libflac FLAC__stream_encoder_process_interleaved
(_fun _FLAC__StreamEncoder-pointer _pointer _uint32_t -> FLAC__bool))
(define (hash-ref/default h k default)
(if (hash-has-key? h k) (hash-ref h k) default))
(define (bool->flac-bool v) (if v 1 0))
(define (native-signed-ref bs start bytes)
(int-bytes->integer bs #t (system-big-endian?) start (+ start bytes)))
(define (scale-sample sample in-bits out-bits)
(cond [(> in-bits out-bits) (arithmetic-shift sample (- out-bits in-bits))]
[(< in-bits out-bits) (arithmetic-shift sample (- out-bits in-bits))]
[else sample]))
(define (pcm-bytes->flac-int32-pointer buffer size channels in-bits out-bits)
(let* ((in-bytes (quotient in-bits 8))
(sample-count (quotient size in-bytes))
(frame-count (quotient sample-count channels))
(ptr (malloc _int32 sample-count 'atomic-interior)))
(for ([i (in-range sample-count)])
(let* ((off (* i in-bytes))
(sample (native-signed-ref buffer off in-bytes)))
(ptr-set! ptr _int32 i (scale-sample sample in-bits out-bits))))
(values ptr frame-count)))
(define (flac-ffi-encoder-handler)
(define enc #f)
(define flac-file #f)
(define settings #f)
(define (require-encoder who)
(when (eq? enc #f) (error who "FLAC encoder is not initialized")))
(define (new)
(if (eq? enc #f)
(begin (set! enc (FLAC__stream_encoder_new)) enc)
(error 'flac-ffi-encoder-handler "FLAC encoder already initialized")))
(define (configure h)
(require-encoder 'flac-encoder-configure)
(set! settings h)
(let ((channels (hash-ref h 'channels))
(sample-rate (hash-ref h 'sample-rate))
(bits (hash-ref h 'bits-per-sample))
(compression-level (hash-ref/default h 'compression-level 5))
(verify? (hash-ref/default h 'verify? #f))
(streamable-subset? (hash-ref/default h 'streamable-subset? (<= (hash-ref h 'bits-per-sample) 24)))
(blocksize (hash-ref/default h 'blocksize 0))
(total-samples (hash-ref/default h 'total-samples #f)))
(unless (FLAC__stream_encoder_set_channels enc channels) (error 'flac-encoder-configure "could not set channels"))
(unless (FLAC__stream_encoder_set_sample_rate enc sample-rate) (error 'flac-encoder-configure "could not set sample rate"))
(unless (FLAC__stream_encoder_set_bits_per_sample enc bits) (error 'flac-encoder-configure "could not set bits per sample"))
(unless (FLAC__stream_encoder_set_compression_level enc compression-level) (error 'flac-encoder-configure "could not set compression level"))
(unless (FLAC__stream_encoder_set_verify enc (bool->flac-bool verify?)) (error 'flac-encoder-configure "could not set verify"))
(unless (FLAC__stream_encoder_set_streamable_subset enc (bool->flac-bool streamable-subset?)) (error 'flac-encoder-configure "could not set streamable subset"))
(when (and (integer? blocksize) (> blocksize 0))
(unless (FLAC__stream_encoder_set_blocksize enc blocksize) (error 'flac-encoder-configure "could not set blocksize")))
(when (and (integer? total-samples) (>= total-samples 0))
(unless (FLAC__stream_encoder_set_total_samples_estimate enc total-samples) (error 'flac-encoder-configure "could not set total samples estimate")))
#t))
(define (init file)
(require-encoder 'flac-encoder-init)
(let ((r (FLAC__stream_encoder_init_file enc file #f #f)))
(set! flac-file file)
(unless (= r FLAC__StreamEncoderInitStatus-ok)
(error 'flac-encoder-init "FLAC encoder init failed with status ~a, state = ~a" r (FLAC__stream_encoder_get_state enc)))
#t))
(define (write buffer size buf-info)
(require-encoder 'flac-encoder-write)
(let* ((channels (hash-ref settings 'channels))
(in-bits (hash-ref/default buf-info 'pcm-bits-per-sample
(hash-ref/default buf-info 'bits-per-sample
(hash-ref settings 'bits-per-sample))))
(out-bits (hash-ref settings 'bits-per-sample)))
(let-values (((ptr frames) (pcm-bytes->flac-int32-pointer buffer size channels in-bits out-bits)))
(unless (FLAC__stream_encoder_process_interleaved enc ptr frames)
(error 'flac-encoder-write "FLAC encoder process_interleaved failed, state ~a" (FLAC__stream_encoder_get_state enc)))
frames)))
(define (finish)
(require-encoder 'flac-encoder-finish)
(FLAC__stream_encoder_finish enc))
(define (delete)
(unless (eq? enc #f)
(FLAC__stream_encoder_delete enc)
(set! enc #f))
#t)
(lambda (cmd . args)
(cond [(eq? cmd 'new) (new)]
[(eq? cmd 'configure) (configure (car args))]
[(eq? cmd 'init) (init (car args))]
[(eq? cmd 'write) (write (car args) (cadr args) (caddr args))]
[(eq? cmd 'finish) (finish)]
[(eq? cmd 'delete) (delete)]
[(eq? cmd 'state) (and enc (FLAC__stream_encoder_get_state enc))]
[(eq? cmd 'file) flac-file]
[(eq? cmd 'settings) settings]
[else (error (format "unknown FLAC encoder command ~a" cmd))])))
); end of module ); end of module
+1 -1
View File
@@ -10,7 +10,7 @@
;(define lib (ffi-lib "/home/hans/tmp/lib/libmpg123.so")) ;(get-lib '("libmpg123") '("0" #f))) ;(define lib (ffi-lib "/home/hans/tmp/lib/libmpg123.so")) ;(get-lib '("libmpg123") '("0" #f)))
(define lib (get-lib '("libmpg123") '("0" #f))) (define lib (get-lib '("mpg123" "libmpg123") (linux-lib-versions '("0" #f) '("0" #f))))
(define-ffi-definer define-libmpg123 lib (define-ffi-definer define-libmpg123 lib
#:default-make-fail make-not-available) #:default-make-fail make-not-available)
+10
View File
@@ -3,10 +3,20 @@
(require "taglib.rkt" (require "taglib.rkt"
"audio-sniffer.rkt" "audio-sniffer.rkt"
"audio-player.rkt" "audio-player.rkt"
(only-in "audio-decoder.rkt"
audio-supported-extensions
audio-supported-formats
audio-decoder-for-extension)
"opusfile-decoder.rkt"
) )
(provide (all-from-out "taglib.rkt") (provide (all-from-out "taglib.rkt")
(all-from-out "audio-sniffer.rkt") (all-from-out "audio-sniffer.rkt")
(all-from-out "audio-player.rkt") (all-from-out "audio-player.rkt")
audio-supported-extensions
audio-supported-formats
audio-decoder-for-extension
current-opusfile-output-format
opusfile-output-format?
) )
+267
View File
@@ -0,0 +1,267 @@
(module opus-encoder racket/base
(require ffi/unsafe
racket/string
"private/utils.rkt"
"taglib.rkt")
(provide opus-encoder-available?
opus-encoder-default-settings
opus-encoder-prepare-settings
opus-encoder-open
opus-encoder-write
opus-encoder-finish)
;; libopusenc handles the Ogg container, OpusHead and OpusTags. The Racket
;; 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")])
(linux-lib-versions '("0" #f))))
(define libopus
(get-lib (case (system-type 'os)
[(windows) '("opus")]
[else '("opus" "libopus")])
(linux-lib-versions '("0" #f))))
(define libopusenc
(get-lib (case (system-type 'os)
[(windows) '("libopusenc")]
[else '("opusenc" "libopusenc")])
(linux-lib-versions '("0" #f))))
(define _OggOpusComments (_cpointer/null 'ogg-opus-comments))
(define _OggOpusEnc (_cpointer/null 'ogg-opus-enc))
(define (ffi-proc name type)
(and libopusenc
(with-handlers ([exn:fail? (lambda (_) #f)])
(get-ffi-obj name libopusenc type))))
(define ope_comments_create (ffi-proc "ope_comments_create" (_fun -> _OggOpusComments)))
(define ope_comments_destroy (ffi-proc "ope_comments_destroy" (_fun _OggOpusComments -> _void)))
(define ope_comments_add (ffi-proc "ope_comments_add" (_fun _OggOpusComments _string/utf-8 _string/utf-8 -> _int)))
(define ope_comments_add_picture_from_memory
(ffi-proc "ope_comments_add_picture_from_memory" (_fun _OggOpusComments _bytes _size _int _string/utf-8 -> _int)))
(define ope_encoder_create_file
(ffi-proc "ope_encoder_create_file"
(_fun _string/utf-8 _OggOpusComments _int32 _int _int (err : (_ptr o _int))
-> (enc : _OggOpusEnc)
-> (values enc err))))
(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)))
(define ope_encoder_ctl/int (ffi-proc "ope_encoder_ctl" (_fun #:varargs-after 2 _OggOpusEnc _int _int -> _int)))
(define OPUS_SET_BITRATE_REQUEST 4002)
(define OPUS_SET_VBR_REQUEST 4006)
(define OPUS_SET_COMPLEXITY_REQUEST 4010)
(define OPUS_SET_VBR_CONSTRAINT_REQUEST 4020)
(define OPUS_SET_SIGNAL_REQUEST 4024)
(define OPUS_SET_LSB_DEPTH_REQUEST 4036)
(define OPE_SET_COMMENT_PADDING_REQUEST 14004)
(define OPUS_AUTO -1000)
(define OPUS_SIGNAL_VOICE 3001)
(define OPUS_SIGNAL_MUSIC 3002)
(define (opus-encoder-available?)
(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)
(define (hash-ref/default h k default)
(if (hash-has-key? h k) (hash-ref h k) default))
(define (copy-hash h)
(let ((out (make-hash)))
(for-each (lambda (k) (hash-set! out k (hash-ref h k))) (hash-keys h))
out))
(define (hash-merge base override)
(let ((out (copy-hash base)))
(when (hash? override)
(for-each (lambda (k) (hash-set! out k (hash-ref override k))) (hash-keys override)))
out))
(define (opus-error-message code)
(if ope_strerror (ope_strerror code) (format "libopusenc error ~a" code)))
(define (check-ope who r)
(when (negative? r) (error who "~a" (opus-error-message r)))
r)
(define (opus-encoder-default-settings)
(make-hash '((bitrate . 160000)
(vbr? . #t)
(constrained-vbr? . #f)
(complexity . 10)
(comment-padding . 512))))
(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 (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)
(hash-set! h 'channels channels)
(hash-set! h 'family 0)
h))
(define (apply-ctl! enc request value who)
(when ope_encoder_ctl/int
(check-ope who (ope_encoder_ctl/int enc request value))))
(define (apply-settings! enc settings)
(apply-ctl! enc OPUS_SET_BITRATE_REQUEST (hash-ref settings 'bitrate) 'opus-bitrate)
(apply-ctl! enc OPUS_SET_VBR_REQUEST (if (hash-ref/default settings 'vbr? #t) 1 0) 'opus-vbr)
(apply-ctl! enc OPUS_SET_VBR_CONSTRAINT_REQUEST (if (hash-ref/default settings 'constrained-vbr? #f) 1 0) 'opus-constrained-vbr)
(apply-ctl! enc OPUS_SET_COMPLEXITY_REQUEST (hash-ref/default settings 'complexity 10) 'opus-complexity)
(apply-ctl! enc OPE_SET_COMMENT_PADDING_REQUEST (hash-ref/default settings 'comment-padding 512) 'opus-comment-padding)
(when (hash-has-key? settings 'signal)
(apply-ctl! enc OPUS_SET_SIGNAL_REQUEST (signal->int (hash-ref settings 'signal)) 'opus-signal))
(when (hash-has-key? settings 'lsb-depth)
(apply-ctl! enc OPUS_SET_LSB_DEPTH_REQUEST (hash-ref settings 'lsb-depth) 'opus-lsb-depth)))
(define (add-comments! comments settings)
(when (hash-has-key? settings 'comments)
(let ((ch (hash-ref settings 'comments)))
(when (hash? ch)
(for-each (lambda (k)
(let ((v (hash-ref ch k)))
(when (string? v)
(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"))
(let* ((file (if (path? output-file) (path->string output-file) output-file))
(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)
(hash-ref resolved 'family))))
(unless enc (error 'opus-encoder-open "could not create Opus file ~a: ~a" file (opus-error-message err)))
(apply-settings! enc resolved)
(make-opus-encoder-handle enc comments resolved format file))))
(define (native-signed-ref bs 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->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 (pcm-bytes->float-pointer buffer size in-bits)
(let* ((in-bytes (quotient in-bits 8))
(sample-count (quotient size in-bytes))
(ptr (malloc _float sample-count 'atomic-interior)))
(for ([i (in-range sample-count)])
(let* ((in-off (* i in-bytes))
(sample (native-signed-ref buffer in-off in-bytes)))
(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 '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
void
(lambda () (check-ope 'opus-encoder-finish (ope_encoder_drain (opus-encoder-handle-enc handle))))
(lambda ()
(ope_encoder_destroy (opus-encoder-handle-enc handle))
(ope_comments_destroy (opus-encoder-handle-comments handle)))))
) ; end of module
+316
View File
@@ -0,0 +1,316 @@
(module opusfile-decoder racket/base
(require ffi/unsafe
"private/utils.rkt")
(provide opusfile-open
opusfile-valid?
opusfile-read
opusfile-stop
opusfile-seek
opusfile-available?
current-opusfile-output-format
opusfile-output-format?)
;; Xiph libopusfile backend for Ogg Opus streams.
;;
;; By default this backend uses op_read(), which returns signed 16-bit
;; interleaved PCM. That is the most efficient path for direct libao
;; playback. For users who prefer the wider decoder output path, set
;; current-opusfile-output-format to 's24. In that mode the backend uses
;; op_read_float() and converts the interleaved float output to packed signed
;; 24-bit PCM in native byte order.
;;
;; Opus decode output is always 48 kHz PCM. The original input rate, if
;; present in metadata, is not the actual decoder output rate.
(define libogg (get-lib (case (system-type 'os)
[(windows) '("ogg")]
[else '("ogg" "libogg")])
(linux-lib-versions '("0" #f))))
(define libopus (get-lib (case (system-type 'os)
[(windows) '("opus")]
[else '("opus" "libopus")])
(linux-lib-versions '("0" #f))))
(define libopusfile (get-lib (case (system-type 'os)
[(windows) '("opusfile")]
[else '("opusfile" "libopusfile")])
(linux-lib-versions '("0" #f))))
(define _OggOpusFile _pointer)
(define default-frames-per-read 4096)
(define opus-sample-rate 48000)
(define (opusfile-output-format? v)
(or (eq? v 's16) (eq? v 's24)))
(define cur-output-format 's16)
(define (current-opusfile-output-format . args)
(unless (null? args)
(if (or (> (length args) 1)
(not (opusfile-output-format? (car args))))
(raise-argument-error 'current-opusfile-output-format
"(or/c 's16 's24)")
(set! cur-output-format (car args))))
cur-output-format)
(define (opus-bits-per-sample)
(case (current-opusfile-output-format)
[(s16) 16]
[(s24) 24]))
(define (opus-bytes-per-sample)
(case (current-opusfile-output-format)
[(s16) 2]
[(s24) 3]))
(define (ffi-proc name type)
(and libopusfile
(with-handlers ([exn:fail? (lambda (_) #f)])
(get-ffi-obj name libopusfile type))))
(define op_open_file
(ffi-proc "op_open_file"
(_fun _path (err : (_ptr o _int))
-> (r : _OggOpusFile)
-> (values r err))))
(define op_free
(ffi-proc "op_free"
(_fun _OggOpusFile -> _void)))
(define op_channel_count
(ffi-proc "op_channel_count"
(_fun _OggOpusFile _int -> _int)))
(define op_pcm_total
(ffi-proc "op_pcm_total"
(_fun _OggOpusFile _int -> _int64)))
(define op_pcm_seek
(ffi-proc "op_pcm_seek"
(_fun _OggOpusFile _int64 -> _int)))
(define op_read
(ffi-proc "op_read"
(_fun _OggOpusFile _bytes _int (li : (_ptr o _int))
-> (r : _int)
-> (values r li))))
(define op_read_float
(ffi-proc "op_read_float"
(_fun _OggOpusFile _pointer _int (li : (_ptr o _int))
-> (r : _int)
-> (values r li))))
(define (opusfile-available?)
(and libopusfile
op_open_file
op_free
op_channel_count
op_pcm_total
op_pcm_seek
op_read
op_read_float
#t))
(define-struct opusfile-handle
(of cb-info cb-audio
(stop #:mutable)
(seek #:mutable)
(reading #:mutable)
(format #:mutable)
(pcm-pos #:mutable))
#:transparent)
(define (raise-opus who fmt . args)
(apply error who fmt args))
(define (check-libopusfile who)
(unless (opusfile-available?)
(raise-opus who "libopusfile could not be loaded")))
(define (correct-format-hash h)
(unless (hash-ref h 'sample-rate #f)
(hash-set! h 'sample-rate opus-sample-rate))
(unless (hash-ref h 'bits-per-sample #f)
(hash-set! h 'bits-per-sample (opus-bits-per-sample)))
(unless (hash-ref h 'bytes-per-sample #f)
(hash-set! h 'bytes-per-sample (opus-bytes-per-sample)))
(unless (hash-ref h 'sample-format #f)
(hash-set! h 'sample-format (current-opusfile-output-format)))
(unless (hash-ref h 'total-samples #f)
(hash-set! h 'total-samples 0)
(hash-set! h 'duration 0)))
(define (report-format handle)
(let ((cb (opusfile-handle-cb-info handle)))
(when (procedure? cb)
(cb (opusfile-handle-format handle)))))
(define (make-format channels total-samples)
(let ((h (make-hash)))
(hash-set! h 'duration (if (and (integer? total-samples) (>= total-samples 0))
(exact->inexact (/ total-samples opus-sample-rate))
0.0))
(hash-set! h 'sample-rate opus-sample-rate)
(hash-set! h 'channels channels)
(hash-set! h 'bits-per-sample (opus-bits-per-sample))
(hash-set! h 'bytes-per-sample (opus-bytes-per-sample))
(hash-set! h 'sample-format (current-opusfile-output-format))
(hash-set! h 'total-samples total-samples)
h))
(define (give-audio handle buffer size)
(let ((h (opusfile-handle-format handle)))
(correct-format-hash h)
(hash-set! h 'sample (opusfile-handle-pcm-pos handle))
(hash-set! h 'current-time (exact->inexact (/ (opusfile-handle-pcm-pos handle)
opus-sample-rate)))
((opusfile-handle-cb-audio handle) h buffer size)))
(define s24-pos-scale #x7FFFFF)
(define s24-neg-scale #x800000)
(define (clip-sample x)
(cond [(< x -1.0) -1.0]
[(> x 1.0) 1.0]
[else x]))
(define (float->s24 x)
(let ((y (clip-sample x)))
(if (negative? y)
(inexact->exact (round (* y s24-neg-scale)))
(inexact->exact (round (* y s24-pos-scale))))))
(define (write-s24-native! bs offset sample)
(let ((v (if (negative? sample) (+ sample #x1000000) sample)))
(if (system-big-endian?)
(begin
(bytes-set! bs offset (bitwise-and (arithmetic-shift v -16) #xFF))
(bytes-set! bs (+ offset 1) (bitwise-and (arithmetic-shift v -8) #xFF))
(bytes-set! bs (+ offset 2) (bitwise-and v #xFF)))
(begin
(bytes-set! bs offset (bitwise-and v #xFF))
(bytes-set! bs (+ offset 1) (bitwise-and (arithmetic-shift v -8) #xFF))
(bytes-set! bs (+ offset 2) (bitwise-and (arithmetic-shift v -16) #xFF))))))
(define (opusfile-valid? audio-file)
(and (opusfile-available?)
(file-exists? audio-file)
#t))
(define (opusfile-open audio-file* cb-stream-info cb-audio)
(check-libopusfile 'opusfile-open)
(let ((audio-file (if (path? audio-file*)
(path->string audio-file*)
audio-file*)))
(if (file-exists? audio-file)
(let-values (((of err) (op_open_file audio-file)))
(if of
(let* ((channels (op_channel_count of -1))
(total-samples (op_pcm_total of -1))
(fmt (make-format channels total-samples))
(h (make-opusfile-handle of cb-stream-info cb-audio #f #f #f fmt 0)))
(report-format h)
h)
(raise-opus 'opusfile-open
"could not open Opus file ~a; opusfile error code: ~a"
audio-file err)))
#f)))
(define (handle-pending-seek! handle)
(unless (eq? (opusfile-handle-seek handle) #f)
(let ((sample (opusfile-handle-seek handle)))
(dbg-sound "Seeking opusfile to sample ~a" sample)
(let ((r (op_pcm_seek (opusfile-handle-of handle) sample)))
(when (negative? r)
(err-sound "opusfile seek error: ~a" r))
(when (not (negative? r))
(set-opusfile-handle-pcm-pos! handle sample)))
(set-opusfile-handle-seek! handle #f))))
(define (read-s16 handle channels)
(let* ((max-samples (* default-frames-per-read channels))
(buffer (make-bytes (* max-samples 2))))
(let-values (((read-frames link-index)
(op_read (opusfile-handle-of handle) buffer max-samples)))
(cond [(negative? read-frames)
(values read-frames #f 0)]
[(zero? read-frames)
(values 0 #f 0)]
[else
(let* ((read-samples (* read-frames channels))
(read-bytes (* read-samples 2))
(out (if (= read-bytes (bytes-length buffer)) buffer (subbytes buffer 0 read-bytes))))
(values read-frames out read-bytes))]))))
(define (read-s24 handle channels)
(let* ((max-samples (* default-frames-per-read channels))
(float-buffer (malloc _float max-samples 'atomic-interior)))
(let-values (((read-frames link-index)
(op_read_float (opusfile-handle-of handle) float-buffer max-samples)))
(cond [(negative? read-frames)
(values read-frames #f 0)]
[(zero? read-frames)
(values 0 #f 0)]
[else
(let* ((read-samples (* read-frames channels))
(out (make-bytes (* read-samples 3))))
(for ([i (in-range read-samples)])
(write-s24-native! out (* i 3) (float->s24 (ptr-ref float-buffer _float i))))
(values read-frames out (bytes-length out)))]))))
(define (read-audio-buffer handle channels)
(case (current-opusfile-output-format)
[(s16) (read-s16 handle channels)]
[(s24) (read-s24 handle channels)]))
(define (opusfile-read handle)
(set-opusfile-handle-stop! handle #f)
(set-opusfile-handle-reading! handle #t)
(let loop ()
(cond
[(opusfile-handle-stop handle)
(dbg-sound "Stopping opusfile decoding")
(set-opusfile-handle-reading! handle #f)
'stopped-reading]
[else
(handle-pending-seek! handle)
(let ((channels (hash-ref (opusfile-handle-format handle) 'channels 2)))
(let-values (((read-frames out read-bytes) (read-audio-buffer handle channels)))
(cond [(negative? read-frames)
(err-sound "opusfile decode error: ~a" read-frames)
(set-opusfile-handle-stop! handle #t)
(loop)]
[(zero? read-frames)
(set-opusfile-handle-stop! handle #t)
(loop)]
[else
(give-audio handle out read-bytes)
(set-opusfile-handle-pcm-pos! handle (+ (opusfile-handle-pcm-pos handle) read-frames))
(loop)])))]))
(op_free (opusfile-handle-of handle))
(set-opusfile-handle-reading! handle #f))
(define (opusfile-seek handle percentage)
(let* ((fmt (opusfile-handle-format handle))
(total-samples (hash-ref fmt 'total-samples 0)))
(unless (or (eq? total-samples #f) (= total-samples -1) (= total-samples 0))
(let* ((percentage (max 0 (min 100 percentage)))
(sample (inexact->exact
(round (* (exact->inexact (/ percentage 100.0))
total-samples)))))
(set-opusfile-handle-seek! handle sample)))))
(define (opusfile-stop handle)
(set-opusfile-handle-stop! handle #t)
(while (opusfile-handle-reading handle)
(sleep 0.01)))
) ; end of module
+2 -2
View File
@@ -9,7 +9,7 @@
"tests.rkt" "tests.rkt"
) )
(define place-mode #f) (define place-mode #t)
(define run-queue #f) (define run-queue #f)
(define (set-test a) (define (set-test a)
@@ -26,7 +26,7 @@
) )
(sprintf "%02d:%02d" minutes seconds))) (sprintf "%02d:%02d" minutes seconds)))
(define (audio-player-state h st) (define (audio-player-state h s st)
(early-return (early-return
((? (not (audio-play? h)) => 'done)) ((? (not (audio-play? h)) => 'done))
(let* ((f (audio-file h)) (let* ((f (audio-file h))
+100
View File
@@ -0,0 +1,100 @@
#lang racket/base
(provide audio-cfg-get
audio-cfg-set!
audio-remote-cfg-get
audio-remote-cfg-set!
)
(require simple-ini/class)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Supporting functions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (build-env-path env . parts)
(let ((base (getenv env)))
(if (eq? base #f)
#f
(apply build-path (cons base parts)))))
(define (try-programs . prgs)
(letrec ((f (λ (l)
(if (null? l)
#f
(if (and (car l) (file-exists? (car l)))
(car l)
(f (cdr l)))))))
(f prgs)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Initialization
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define cfg (new ini% [file 'racket-audio]))
(when (eq? (send cfg get 'init 'initialized #f) #f)
(let ((s! (λ (e v)
(send cfg set! 'racket-audio e v))))
(s! 'remote-racket "racket")
(s! 'remote-module "racket-audio/audio-placed-player")
(s! 'ssh-program (cond
((eq? (system-type 'os) 'windows)
(let ((p (try-programs
(build-env-path "ProgramFiles" "PuTTY" "plink.exe")
(build-env-path "ProgramFiles" "OpenSSH" "ssh.exe")
(build-env-path "SystemRoot" "System32" "OpenSSH" "ssh.exe")
(find-executable-path "ssh.exe")
(find-executable-path "plink.exe"))))
(if (eq? p #f)
"plink.exe"
p)))
(else (let ((p (find-executable-path "ssh")))
(if (eq? p #f)
"ssh"
p))))
)
(send cfg set! 'init 'initialized #t)
)
)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Provided API
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (missing-cfg-value? v)
(eq? v '@racket-audio-no-value@))
(define (audio-getter section entry default-provider)
(let ((v (send cfg get section entry '@racket-audio-no-value@)))
(if (missing-cfg-value? v)
(cond ((null? default-provider)
(error (format
"audio-cfg-get: No such entry ~a in section ~a"
section entry)))
((procedure? (car default-provider))
((car default-provider) entry))
(else
(car default-provider)))
v)))
(define (audio-cfg-get entry . default-provider)
(audio-getter 'racket-audio entry default-provider))
(define (audio-cfg-set! entry val)
(send cfg set! 'racket-audio entry val))
(define (audio-remote-cfg-get host entry . default-provider)
(audio-getter (string->symbol (format "~a" host))
entry
(list (lambda (entry)
(apply audio-cfg-get (cons entry default-provider))))))
(define (audio-remote-cfg-set! host entry val)
(send cfg set! (string->symbol (format "~a" host)) entry val))
+5 -5
View File
@@ -26,8 +26,8 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define version-major 1) (define version-major 1)
(define version-minor 0) (define version-minor 1)
(define version-patch 0) (define version-patch 2)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Internal functions ;; Internal functions
@@ -40,8 +40,8 @@
version-patch version-patch
)) ))
(define download-site "git.dijkewijk.nl") (define download-site "codeberg.org")
(define base-path "hans/racket-sound-lib/releases/download") (define base-path "hnmdijkema/racket-sound-lib/releases/download")
(define os (system-type 'os*)) (define os (system-type 'os*))
(define arch (system-type 'arch)) (define arch (system-type 'arch))
@@ -145,7 +145,7 @@
(let* ((file (build-path install-path "archive.zip")) (let* ((file (build-path install-path "archive.zip"))
(out (open-output-file file #:exists 'replace)) (out (open-output-file file #:exists 'replace))
) )
(displayln (format "Downloading racket-webview-qt (~a)..." download-url)) (displayln (format "Downloading racket-sound-lib (~a)..." download-url))
(do-download in out) (do-download in out)
(displayln (format "downloaded '~a'" file)) (displayln (format "downloaded '~a'" file))
(when (directory-exists? ffi-path) (when (directory-exists? ffi-path)
+150
View File
@@ -0,0 +1,150 @@
(module pcm-converter racket/base
(require ffi/unsafe
"utils.rkt"
"../resampler.rkt")
(provide pcm-conversion-needed?
make-pcm-converter
pcm-converter?
pcm-converter-input-format
pcm-converter-output-format
pcm-converter-convert
pcm-converter-drain
pcm-converter-close!)
(define S32-BYTES 4)
(define-struct pcm-converter (resampler input-format output-format channels in-rate out-rate closed?)
#:mutable
#:constructor-name make-raw-pcm-converter)
(define (hash-ref/default h k default)
(if (and (hash? h) (hash-has-key? h k)) (hash-ref h k) default))
(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 (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 (expand-sample-to-s32 sample in-bits)
(clamp-s32 (if (< in-bits 32) (arithmetic-shift sample (- 32 in-bits)) sample)))
(define (pcm-bytes->s32-bytes buffer size in-bits)
(cond [(= in-bits 32) (if (= size (bytes-length buffer)) buffer (subbytes buffer 0 size))]
[else
(let* ((in-bytes (quotient in-bits 8))
(sample-count (quotient size in-bytes))
(out (make-bytes (* sample-count S32-BYTES))))
(for ([i (in-range sample-count)])
(let* ((in-off (* i in-bytes))
(out-off (* i S32-BYTES))
(sample (native-signed-ref buffer in-off in-bytes)))
(native-signed-set! out out-off S32-BYTES (expand-sample-to-s32 sample in-bits))))
out)]))
(define (source-value v source)
(if (eq? v 'source) source v))
(define (target-sample-rate settings input-format)
(source-value
(hash-ref/default settings 'target-sample-rate
(hash-ref/default settings 'sample-rate
(hash-ref input-format 'sample-rate)))
(hash-ref input-format 'sample-rate)))
(define (target-channels settings input-format)
(source-value
(hash-ref/default settings 'target-channels
(hash-ref/default settings 'channels
(hash-ref input-format 'channels)))
(hash-ref input-format 'channels)))
(define (target-bits settings input-format)
(let ((source-bits (let ((bits (hash-ref/default input-format 'bits-per-sample 24)))
(if (and (integer? bits) (<= bits 24)) bits 24))))
(source-value
(hash-ref/default settings 'target-bits-per-sample
(hash-ref/default settings 'bits-per-sample source-bits))
source-bits)))
(define (make-output-format input-format settings)
(let* ((out (copy-hash input-format))
(in-rate (hash-ref input-format 'sample-rate))
(out-rate (target-sample-rate settings input-format))
(total (hash-ref/default input-format 'total-samples #f)))
(hash-set! out 'sample-rate out-rate)
(hash-set! out 'channels (target-channels settings input-format))
(hash-set! out 'bits-per-sample (target-bits settings input-format))
(hash-set! out 'pcm-bits-per-sample 32)
(hash-set! out 'type 'interleaved)
(hash-set! out 'endianness 'native-endian)
(when (and (integer? total) (>= total 0) (integer? in-rate) (> in-rate 0))
(hash-set! out 'total-samples (inexact->exact (round (* total (/ out-rate in-rate))))))
out))
(define (pcm-conversion-needed? input-format settings)
(let ((in-rate (hash-ref input-format 'sample-rate))
(in-channels (hash-ref input-format 'channels))
(out-rate (target-sample-rate settings input-format))
(out-channels (target-channels settings input-format)))
(or (not (= in-rate out-rate))
(not (= in-channels out-channels)))))
(define (make-pcm-converter input-format settings)
(let* ((channels-in (hash-ref input-format 'channels))
(channels-out (target-channels settings input-format))
(rate-in (hash-ref input-format 'sample-rate))
(rate-out (target-sample-rate settings input-format))
(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 (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)))
(in-bytes (quotient in-bits 8))
(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)))
(resampler-convert (pcm-converter-resampler c) s32 (* in-samples in-channels S32-BYTES))))
(define (pcm-converter-drain c)
(ensure-open! c 'pcm-converter-drain)
(resampler-drain (pcm-converter-resampler c)))
(define (pcm-converter-close! c)
(when (and (pcm-converter? c) (not (pcm-converter-closed? c)))
(resampler-close! (pcm-converter-resampler c))
(set-pcm-converter-closed?! c #t))
#t)
) ; end of module
+109
View File
@@ -0,0 +1,109 @@
#lang racket/base
(require racket/path
racket/string
port-channel
uni-channel
"config.rkt"
"utils.rkt"
)
(provide replace-base-paths?
replace-base-path
start-remote-placed-player)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Remote path replacement
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (path-string->string p)
(if (path? p) (path->string p) p))
(define (replace-base-paths? v)
(and (list? v)
(andmap (lambda (entry)
(and (pair? entry)
(path-string? (car entry))
(path-string? (cdr entry))))
v)))
(define (get-path-separator s)
(let ((m (regexp-match #rx"[\\\\/]" s)))
(if (eq? m #f)
(begin
(warn-sound "No delimiter found in ~a, assuming unix: '/'" s)
"/")
(car m))))
(define (replace-base-path path replace-base-paths)
(let ((s (path-string->string path)))
(let loop ((entries replace-base-paths))
(cond [(null? entries) s]
[else
(let* ((entry (car entries))
(base-path-local (path-string->string (car entry)))
(base-path-remote (path-string->string (cdr entry))))
(if (string-prefix? s base-path-local)
(let* ((new-path* (string-append base-path-remote
(substring s (string-length base-path-local))))
(delim (get-path-separator new-path*))
(new-path (string-replace (string-replace new-path* "/" delim) "\\" delim))
)
new-path
)
(loop (cdr entries))))]))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Remote placed player
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (executable-name p)
(let ((p* (if (path? p) p (string->path p))))
(path->string (file-name-from-path p*))))
(define (plink-program? p)
(regexp-match? #rx"(?i:^plink(\\.exe)?$)" (executable-name p)))
(define (ssh-options ssh-program)
(if (plink-program? ssh-program)
'("-batch" "-T" "-load")
'("-T" "-q")))
(define (shell-quote s)
(string-append "'"
(regexp-replace* #rx"'" s "'\"'\"'")
"'"))
(define (remote-command remote-host #:cmd [cmd #f])
(if (eq? cmd #f)
(list (audio-remote-cfg-get remote-host 'remote-racket)
"-l"
(audio-remote-cfg-get remote-host 'remote-module)
"--"
"--stdio")
(list (audio-remote-cfg-get remote-host 'remote-racket)
"-e"
(shell-quote cmd)
"--"
"--stdio")
)
)
(define (start-ssh-subprocess remote-host #:cmd [cmd #f])
(let* ((ssh-program (audio-cfg-get 'ssh-program))
(args (append (ssh-options ssh-program)
(list remote-host)
(remote-command remote-host #:cmd cmd))))
(apply subprocess #f #f #f ssh-program args)))
(define (make-port-uc port direction source)
(make-uni-channel (make-port-channel port #:direction direction #:source source #:close? #t)))
(define (start-remote-placed-player remote-host)
(define-values (proc stdout stdin stderr)
(start-ssh-subprocess remote-host))
(define cmd-ch (make-port-uc stdin 'output 'remote-stdin))
(define ret-ch (make-port-uc stdout 'input 'remote-stdout))
(define evt-ch (make-port-uc stderr 'input 'remote-stderr))
(define dead-guard (lambda () (subprocess-wait proc)))
(values cmd-ch ret-ch evt-ch proc dead-guard))
+127 -68
View File
@@ -1,7 +1,9 @@
(module utils racket/base (module utils racket/base
(require racket/path (require racket/path
racket/file
racket/runtime-path racket/runtime-path
racket/system
ffi/unsafe ffi/unsafe
setup/dirs setup/dirs
"downloader.rkt" "downloader.rkt"
@@ -12,6 +14,9 @@
until until
build-lib-path build-lib-path
get-lib get-lib
get-lib/quiet
linux-lib-versions
ffmpeg-lib-versions
do-for do-for
dbg-sound dbg-sound
info-sound info-sound
@@ -19,6 +24,9 @@
warn-sound warn-sound
fatal-sound fatal-sound
sync-log-sound sync-log-sound
racket-sound-cache-directory
racket-sound-log-file
open-racket-sound-log-file
integer->int-bytes integer->int-bytes
int-bytes->integer int-bytes->integer
valid-ffmpeg-versions valid-ffmpeg-versions
@@ -34,6 +42,30 @@
(sl-def-log racket-sound sound) (sl-def-log racket-sound sound)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Standard cache/log paths
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (racket-sound-cache-directory)
(build-path (find-system-path 'cache-dir) "racket-audio"))
(define (racket-sound-log-file name)
(define filename
(cond [(symbol? name) (format "~a.log" name)]
[(string? name) name]
[else (raise-argument-error 'racket-sound-log-file "(or/c symbol? string?)" name)]))
(build-path (racket-sound-cache-directory) filename))
(define (open-racket-sound-log-file path #:exists [exists 'append])
(define parent (path-only (path->complete-path path)))
(when parent (make-directory* parent))
(open-output-file path #:exists exists))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Mutex definitions ;; Mutex definitions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -135,75 +167,99 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define valid-ffmpeg-versions (define valid-ffmpeg-versions
(make-hash (list (list 'avutil 58 60 "libavcodec") (make-hash (list (list 'avutil 58 60 "libavutil")
(list 'avcodec 60 62 "libavutil") (list 'avcodec 60 62 "libavcodec")
(list 'avformat 60 62 "libswresample") (list 'avformat 60 62 "libavformat")
(list 'swresample 4 6 "libavformat") (list 'swresample 4 6 "libswresample"))))
))
)
(define (version-str kind) (define (versions-high-to-low from until)
(let loop ((n from) (acc '(#f)))
(if (> n until)
acc
(loop (+ n 1) (cons (number->string n) acc)))))
(define (ffmpeg-lib-versions kind)
(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*)
[(linux) versions]
[else default]))
(define (ffmpeg-version-line kind)
(let ((v (hash-ref valid-ffmpeg-versions kind))) (let ((v (hash-ref valid-ffmpeg-versions kind)))
(format " - ~a~a - ~a~a\n" (caddr v) (car v) (caddr v) (cadr v)) (format " ~a: ~a .. ~a" (caddr v) (car v) (cadr v))))
)
) (define (display-section title lines)
(displayln title)
(for ([line (in-list lines)]) (displayln line))
(newline))
(define (display-command title chunks)
(displayln title)
(displayln " sudo apt install \\")
(let loop ((xs chunks))
(cond [(null? xs) (newline)]
[(null? (cdr xs))
(displayln (format " ~a" (car xs)))
(newline)]
[else
(displayln (format " ~a \\" (car xs)))
(loop (cdr xs))])))
(define linux-runtime-package-lines
'("libflac12 libmpg123-0 libao4"
"libavcodec60 libavutil58 libswresample4 libavformat60"
"libogg0 libopus0 libopusenc0 libopusfile0"
"libsoxr0 libtag1v5"))
(define linux-dev-package-lines
'("libflac-dev libmpg123-dev libao-dev"
"libavcodec-dev libavutil-dev libswresample-dev libavformat-dev"
"libogg-dev libopus-dev libopusenc-dev libopusfile-dev"
"libsoxr-dev libtag1-dev"))
(define brew-package-lines
'("ffmpeg libao mpg123 flac opus"
"libopusenc libsoxr taglib"))
(define (lib-not-found-message orig-libs libs-path) (define (lib-not-found-message orig-libs libs-path)
(displayln (format "Warning: Cannot find library, tried ~a in ~a" orig-libs libs-path)) (newline)
(displayln "Warning: native library not found")
(newline)
(display-section "Tried library names:"
(for/list ([lib (in-list orig-libs)]) (format " - ~a" lib)))
(display-section "Search paths:"
(for/list ([path (in-list libs-path)]) (format " - ~a" path)))
(let ((st (system-type 'os*))) (let ((st (system-type 'os*)))
(cond ((eq? st 'windows) (cond [(eq? st 'windows)
(displayln (display-section "Windows native library directory:"
(format (list (format " - ~a" (soundlibs-directory))))]
"\nLibraries for Windows should have been downloaded to\n\n - ~a\n" [(eq? st 'linux)
(soundlibs-directory)))) (display-command "Debian/Ubuntu runtime packages:" linux-runtime-package-lines)
((eq? st 'linux) (display-command "Debian/Ubuntu development packages, for local FFI rebuilding:" linux-dev-package-lines)]
(displayln [(eq? st 'macosx)
(string-append (display-command "Homebrew packages:" brew-package-lines)
"Make sure you have installed the following libraries,\n" (displayln "If your local setup uses ffmpeg-full instead of ffmpeg, install that variant instead.")
"e.g. on a debian based system with apt:\n" (newline)]
"\n" [else
" FLAC : sudo apt install libflac12\n" (display-section "Required native libraries:"
" mpg123 : libmpg123-0\n" '(" - xiph libao"
" libao : libao4\n" " - xiph libFLAC"
" ffmpeg : libavcodec60 libavutil58 libswresample4 libavformat60\n" " - ffmpeg"
"\n" " - libmpg123"
))) " - libopus/libopusenc/libopusfile"
((eq? st 'macosx) " - libsoxr"
(displayln " - taglib"))])
(string-append (display-section "Supported FFmpeg ABI versions:"
"Make sure you have the right libraries installed, using 'homebrew', see https://brew.sh/\n" (list (ffmpeg-version-line 'avcodec)
"\n" (ffmpeg-version-line 'avutil)
" brew install ffmpeg-full\n" (ffmpeg-version-line 'swresample)
" brew install libao\n" (ffmpeg-version-line 'avformat)))))
" brew install mpg123\n"
" brew install flac\n"
"\n"
)))
(else
(displayln
(string-append
"Make sure you have the right libraries installed on your system and reachable by racket\n"
"\n"
"You need following libraries:\n"
"\n"
"- xiph libao (https://xiph.org).\n"
"- xiph libFLAC (https://xiph.org).\n"
"- ffmpeg of the right version (https://ffmpeg.org).\n"
"- libmpg123 (https://mpg123.org).\n"
"\n")
))
)
(displayln
(string-append "NB. currently supported major versions for the ffmpeg libraries are:\n"
"\n"
(version-str 'avcodec)
(version-str 'avutil)
(version-str 'swresample)
(version-str 'avformat)
"\n"
))
)
)
(define (build-lib-path p) (define (build-lib-path p)
(if (eq? (system-type 'os) 'macosx) (if (eq? (system-type 'os) 'macosx)
@@ -211,7 +267,7 @@
(cons (soundlibs-directory) (cons brew-lib p))) (cons (soundlibs-directory) (cons brew-lib p)))
(cons (soundlibs-directory) p))) (cons (soundlibs-directory) p)))
(define (get-lib* libs-to-try orig-libs versions) (define (get-lib* libs-to-try orig-libs versions warn?)
(unless (soundlibs-available?) (unless (soundlibs-available?)
(download-soundlibs)) (download-soundlibs))
@@ -219,21 +275,24 @@
(let ((libs-path (build-lib-path (get-lib-search-dirs)))) (let ((libs-path (build-lib-path (get-lib-search-dirs))))
(if (null? libs-to-try) (if (null? libs-to-try)
(begin (begin
(lib-not-found-message orig-libs libs-path) (when warn? (lib-not-found-message orig-libs libs-path))
#f) #f)
(ffi-lib (car libs-to-try) versions (ffi-lib (car libs-to-try) versions
#:get-lib-dirs (λ () libs-path) #:get-lib-dirs (λ () libs-path)
#:fail (λ () #:fail (λ ()
(ffi-lib (car libs-to-try) versions (ffi-lib (car libs-to-try) versions
#:fail (λ () #:fail (λ ()
(get-lib* (cdr libs-to-try) orig-libs versions)))) (get-lib* (cdr libs-to-try) orig-libs versions warn?))))
) )
) )
) )
) )
(define (get-lib libs-to-try versions) (define (get-lib libs-to-try versions)
(get-lib* libs-to-try libs-to-try versions)) (get-lib* libs-to-try libs-to-try versions #t))
(define (get-lib/quiet libs-to-try versions)
(get-lib* libs-to-try libs-to-try versions #f))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+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
+81 -13
View File
@@ -2,8 +2,10 @@
@(require racket/base @(require racket/base
(for-label racket/base (for-label racket/base
racket/contract
racket/path racket/path
"../audio-decoder.rkt")) "../audio-decoder.rkt"
"../audio-sniffer.rkt"))
@title{audio-decoder} @title{audio-decoder}
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]] @author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
@@ -15,9 +17,9 @@ decoders. A backend is selected from the filename extension and is then
used through a uniform interface for opening, reading, seeking, and used through a uniform interface for opening, reading, seeking, and
stopping. stopping.
The module includes built-in readers for FLAC and MP3, and it allows The module includes built-in readers for FLAC, MP3, Opus via
additional backends to be registered with @tt{libopusfile}, and FFmpeg-backed formats. Additional backends can be
@racket[audio-register-reader!]. registered with @racket[audio-register-reader!].
@section{Reader registration} @section{Reader registration}
@@ -67,7 +69,7 @@ available to @racket[audio-open].
This procedure is the extension point for custom audio decoders. This procedure is the extension point for custom audio decoders.
} }
@section{Audio handles} @section[#:tag "audio-decoder-audio-handles"]{Audio handles}
@defproc[(audio-handle? [v any/c]) boolean?]{ @defproc[(audio-handle? [v any/c]) boolean?]{
@@ -79,8 +81,8 @@ otherwise.
Returns the reader type stored in @racket[handle]. Returns the reader type stored in @racket[handle].
For the built-in readers this is either @racket['flac] or For the built-in readers this is usually @racket['flac], @racket['mp3],
@racket['mp3]. @racket['opusfile], or @racket['ffmpeg].
} }
@section{Known extensions and validation} @section{Known extensions and validation}
@@ -89,9 +91,74 @@ For the built-in readers this is either @racket['flac] or
Returns the list of known filename extensions. Returns the list of known filename extensions.
The initial list contains @racket["flac"] and @racket["mp3"]. The initial list contains the extensions handled by the built-in readers:
Additional extensions are added when readers are registered with @racket["flac"], @racket["mp3"], Opus/Ogg extensions such as
@racket[audio-register-reader!]. @racket["opus"], @racket["ogg"], and @racket["oga"], and the extensions
handled by the FFmpeg reader. Additional extensions are added when
readers are registered with @racket[audio-register-reader!].
}
@defproc[(audio-supported-extensions) (listof string?)]{
Returns the current list of supported filename extensions. This is the
same list returned by @racket[audio-known-exts?].
}
@defproc[(audio-supported-formats)
(listof (cons/c string? string?))]{
Returns the supported filename extensions together with the MIME type
reported by @racketmodname[racket-mimetypes]. Unknown MIME types use
@racket["application/octet-stream"].
}
@defproc[(audio-decoder-for-extension
[extension (or/c string? symbol?)])
(or/c #f symbol?)]{
Returns the decoder selected for audio with @racket[extension], such as
@racket['flac], @racket['opusfile], or @racket['ffmpeg]. The result is
@racket[#f] when the extension is not recognized. Actual playback still
sniffs the file contents before selecting its decoder.
}
@section{Built-in decoder selection}
The built-in decoder table contains:
@itemlist[#:style 'compact
@item{@racket['flac] for FLAC files;}
@item{@racket['mp3] for MP3 files;}
@item{@racket['opusfile] for Ogg Opus streams through Xiph
@tt{libopusfile};}
@item{@racket['ffmpeg] for formats handled by the FFmpeg backend.}]
When a file is opened, the module first asks
@racket[audio-sniff-format/extension] for the detected format. Opus
streams are mapped to @racket['opusfile] when @tt{libopusfile} is
available and to @racket['ffmpeg] otherwise.
The Opusfile decoder has one global setting for its PCM output format:
@racket[current-opusfile-output-format]. This setting is re-exported by
this module for callers that use only the generic decoder API.
@defthing[current-opusfile-output-format procedure?]{
A global setting procedure for the Opusfile backend. Called without
arguments, it returns the current Opusfile output format. Called with one
argument, it sets the output format. Accepted values are @racket['s16]
and @racket['s24].
The default is @racket['s16]. Set it to @racket['s24] before opening or
reading an Opus file when the playback pipeline should receive packed
signed 24-bit PCM instead of signed 16-bit PCM.
}
@defproc[(opusfile-output-format? [v any/c]) boolean?]{
Returns @racket[#t] when @racket[v] is one of the supported Opusfile
output format symbols: @racket['s16] or @racket['s24].
} }
@defproc[(audio-valid-ext? [ext any/c]) boolean?]{ @defproc[(audio-valid-ext? [ext any/c]) boolean?]{
@@ -146,7 +213,8 @@ where:
@itemlist[#:style 'compact @itemlist[#:style 'compact
@item{@racket[audio-type] is the registered reader type, such as @item{@racket[audio-type] is the registered reader type, such as
@racket['flac] or @racket['mp3];} @racket['flac], @racket['mp3], @racket['opusfile], or
@racket['ffmpeg];}
@item{@racket[ao-type] is the audio-output type stored in the reader, @item{@racket[ao-type] is the audio-output type stored in the reader,
such as @racket['flac] or @racket['ao];} such as @racket['flac] or @racket['ao];}
@item{@racket[handle] is the generic @racket[audio-handle];} @item{@racket[handle] is the generic @racket[audio-handle];}
@@ -255,5 +323,5 @@ A backend integrated through this interface should provide:
backend produces.}] backend produces.}]
Once registered, files with matching extensions can be opened through Once registered, files with matching extensions can be opened through
@racket[audio-open] in the same way as the built-in FLAC and MP3 @racket[audio-open] in the same way as the built-in FLAC, MP3,
backends. Opusfile, and FFmpeg backends.
+231
View File
@@ -0,0 +1,231 @@
#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
read-write API from @racketmodname[racket-audio/taglib].
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
sample rate differs from the decoded input format, @tt{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:
@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].}
+60 -16
View File
@@ -5,6 +5,8 @@
racket/contract racket/contract
racket/place racket/place
racket/async-channel racket/async-channel
uni-channel
port-channel
"../audio-placed-player.rkt" "../audio-placed-player.rkt"
"../audio-player.rkt")) "../audio-player.rkt"))
@@ -30,9 +32,11 @@ The placed player is implemented as a command loop around a decoder, an
asynchronous libao output handle, and a small amount of state that is reported asynchronous libao output handle, and a small amount of state that is reported
back to the controlling side. In normal use it runs in a Racket place, so that back to the controlling side. In normal use it runs in a Racket place, so that
the audio side has a separate Racket VM. The same function can also run in a the audio side has a separate Racket VM. The same function can also run in a
normal Racket thread with async channels. That mode is useful for debugging, normal Racket thread with async channels. Both modes are mediated through
because the player then stays in the same process and can be inspected more @racketmodname[uni-channel]: each of the existing logical channels is wrapped as
easily. a uni-channel endpoint, so the worker no longer depends directly on
@racket[place-channel-put], @racket[place-channel-get],
@racket[async-channel-put], or @racket[async-channel-get].
It is normally run in a separate place so that audio decoding and feeding are It is normally run in a separate place so that audio decoding and feeding are
isolated from scheduling delays in the main Racket VM, such as GUI activity, isolated from scheduling delays in the main Racket VM, such as GUI activity,
@@ -40,22 +44,58 @@ debugging, or interaction with DrRacket.
@section{Interface} @section{Interface}
@defproc[(placed-player [ch-in (or/c place-channel? async-channel?)]) void?]{ @defproc[(placed-player
Runs the placed-player command loop on @racket[ch-in]. The channel may be a [ch-in (or/c uni-channel? place-channel? async-channel? port-channel?)]
place channel or an async channel. The command loop receives list commands, [ch-out (or/c #f uni-channel? place-channel? async-channel? port-channel?) #f]
[ch-evt (or/c #f uni-channel? place-channel? async-channel? port-channel?) #f])
void?]{
Runs the placed-player command loop on @racket[ch-in]. Each channel may already
be a @racket[uni-channel?] or may be a supported raw channel that can be wrapped
with @racket[make-uni-channel]. The command loop receives list commands,
initializes its reply and event channels, and then processes playback commands initializes its reply and event channels, and then processes playback commands
until it receives @racket['quit]. until it receives @racket['quit].
The function is designed to be started either by @racket[dynamic-place] or by When @racket[ch-out] and @racket[ch-evt] are @racket[#f], the command loop
@racket[thread]. In place mode, all three channels are place channels. In expects an initial @racket['init] command containing the raw reply and event
thread mode, all three channels are async channels. The implementation detects channels. This remains the normal path for @racket[dynamic-place], because a
the kind of channel and uses @racket[place-channel-put], @racket[uni-channel] value contains procedures and must not be sent through a
@racket[place-channel-get], @racket[async-channel-put], or place channel. In that case the controlling side sends the raw place channels
@racket[async-channel-get] as appropriate.} and the worker wraps them locally.
When all three channels are supplied, the worker starts initialized. This is
used by the standard-port worker entry point and can also be used by custom
launchers.}
@defproc[(placed-player/stdio [#:log-file log-file (or/c #f path-string?) (racket-sound-log-file 'placed-audio-player-stdio)])
void?]{
Runs @racket[placed-player] using the process standard streams as three logical
channels: @racket[current-input-port] is the command channel,
@racket[current-output-port] is the reply channel, and
@racket[current-error-port] is the asynchronous event channel. The streams are
wrapped through @racket[make-port-channel] and then through
@racket[make-uni-channel].
Because stdout and stderr are protocol streams in this mode, ordinary display
and log output is redirected while the worker is running. By default, output is
appended to a log file under Racket's standard cache directory, for example
@filepath{~/.cache/racket/racket-audio/placed-audio-player-stdio.log} on many
Unix-like systems. Pass @racket[#f] explicitly to discard ordinary output, or
pass a path to choose a different log file.
The module also has a command-line entry point. A remote or local subprocess
worker can be started with:
@verbatim{
racket -l racket-audio/audio-placed-player -- --stdio
}
This is the command used by @racket[make-audio-player] when SSH remote playback
is enabled, unless the caller supplies a custom remote command.}
The public wrapper in @racketmodname[racket-audio/audio-player] creates the channels, The public wrapper in @racketmodname[racket-audio/audio-player] creates the channels,
sends the initial @racket['init] command, starts an event thread, and exposes a sends the initial @racket['init] command when needed, starts an event thread, and exposes a
contracted API. The placed player itself only exports @racket[placed-player]. contracted API. The placed player exports @racket[placed-player] and the
standard-port worker entry point @racket[placed-player/stdio].
@section{Overall state model} @section{Overall state model}
@@ -91,7 +131,10 @@ installed.
The controlling side sends commands as lists on @racket[ch-in]. The result of The controlling side sends commands as lists on @racket[ch-in]. The result of
an RPC-style command is sent on the reply channel installed by an RPC-style command is sent on the reply channel installed by
@racket['init]. Asynchronous events are sent on the event channel. @racket['init] or supplied directly to @racket[placed-player]. Asynchronous
events are sent on the event channel. These are logical channels; the concrete
transport may be place channels, async channels, or port channels wrapped as
uni-channels.
@itemlist[#:style 'compact @itemlist[#:style 'compact
@item{@racket[(list 'init ch-out ch-evt)] installs @racket[ch-out] and @item{@racket[(list 'init ch-out ch-evt)] installs @racket[ch-out] and
@@ -252,7 +295,8 @@ place or thread then terminates.
@section{Running in a place or in a thread} @section{Running in a place or in a thread}
The normal path in @racket[make-audio-player] uses @racket[dynamic-place] when The normal path in @racket[make-audio-player] uses @racket[dynamic-place] when
places are enabled. This gives the audio side its own Racket VM and isolates places are enabled. The command, reply, and event channels are wrapped with
@racket[make-uni-channel] on each side. This gives the audio side its own Racket VM and isolates
it from the main controller, while the command and event protocol stays the it from the main controller, while the command and event protocol stays the
same. same.
+34 -3
View File
@@ -28,7 +28,9 @@ through callbacks supplied when the player is created.
@defproc[(make-audio-player @defproc[(make-audio-player
[cb-state procedure?] [cb-state procedure?]
[cb-eof-stream procedure?] [cb-eof-stream procedure?]
[#:use-place use-place boolean?]) [#:use-place use-place boolean?]
[#:remote-host remote-host (or/c #f string?) #f]
[#:replace-base-paths replace-base-paths any/c '()])
audio-play?]{ audio-play?]{
Creates an audio player and returns a player handle. The handle is passed to Creates an audio player and returns a player handle. The handle is passed to
all other procedures in this module. all other procedures in this module.
@@ -108,7 +110,36 @@ a separate Racket VM, so decoding and buffer feeding are less exposed to
scheduling delays caused by DrRacket, GUI event handling, debugging, logging, or scheduling delays caused by DrRacket, GUI event handling, debugging, logging, or
other active threads in the main VM. Those delays can otherwise be heard as other active threads in the main VM. Those delays can otherwise be heard as
clicks, gaps, or stuttering playback. Thread mode is useful for debugging the clicks, gaps, or stuttering playback. Thread mode is useful for debugging the
protocol and callbacks, but it is not the preferred mode for robust playback.} protocol and callbacks, but it is not the preferred mode for robust playback.
When @racket[remote-host] is a string, @racket[make-audio-player] starts the
worker over SSH instead of starting a local place or thread. All SSH and remote
Racket details are handled by the remote utility layer and its
@tt{racket-audio.ini} configuration. The public player API only needs the host
name.
The remote player must be able to open the requested audio files. When the
local and remote file trees differ, use @racket[replace-base-paths]. It is a
list of cons pairs. The @racket[car] is the local base path, and the
@racket[cdr] is the remote base path. The first matching local prefix is
replaced before the @racket['open] command is sent. For example:
@racketblock[
(make-audio-player cb-state cb-eof
#:remote-host "nas"
#:replace-base-paths
(list (cons "/muziek" "/volume1/music")))]
With that mapping, @filepath{/muziek/klassiek/x.flac} is sent to the remote
worker as @filepath{/volume1/music/klassiek/x.flac}.}
@defproc[(replace-base-path [path path-string?]
[replace-base-paths any/c])
string?]{
Applies the same base-path replacement used by remote playback. This is a small
helper for testing the mapping before starting playback.}
@defproc[(audio-play? [v any/c]) boolean?]{ @defproc[(audio-play? [v any/c]) boolean?]{
@@ -308,7 +339,7 @@ The RPC command path is protected by a mutex in the wrapper. This allows
different application threads to call playback procedures on the same handle different application threads to call playback procedures on the same handle
without interleaving the command and reply parts of a single RPC. without interleaving the command and reply parts of a single RPC.
@section{Example} @section[#:tag "audio-player-example"]{Example}
The following example creates a player, prints state changes, plays a file, and The following example creates a player, prints state changes, plays a file, and
then shuts the player down explicitly. then shuts the player down explicitly.
+1 -1
View File
@@ -15,7 +15,7 @@ file contents (signature sniffing) and, optionally, file extensions.
The sniffer prefers binary inspection over extensions and only falls back The sniffer prefers binary inspection over extensions and only falls back
to extensions when detection is inconclusive. to extensions when detection is inconclusive.
@section{Overview} @section[#:tag "audio-sniffer-overview"]{Overview}
The detection strategy is as follows: The detection strategy is as follows:
+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 -2
View File
@@ -12,7 +12,7 @@
@defmodule[racket-audio/ffmpeg-decoder] @defmodule[racket-audio/ffmpeg-decoder]
This module provides an audio decoder based on the FFmpeg audio shim. It This module provides an audio decoder based on the FFmpeg audio shim. It
uses the lower-level @racketmodname[racket-sound/ffmpeg-ffi] module and presents a uses the lower-level @racketmodname[racket-audio/ffmpeg-ffi] module and presents a
callback-based decoder interface comparable to the other audio decoders. callback-based decoder interface comparable to the other audio decoders.
The native FFmpeg layer decodes audio to signed 32-bit interleaved PCM. The native FFmpeg layer decodes audio to signed 32-bit interleaved PCM.
@@ -121,7 +121,7 @@ Seeking is asynchronous with respect to @racket[ffmpeg-seek]: the
function only records the requested target sample. The read loop applies function only records the requested target sample. The read loop applies
the pending seek request before decoding the next block. the pending seek request before decoding the next block.
@section{Notes} @section[#:tag "ffmpeg-decoder-notes"]{Notes}
The FFmpeg shim output is expected to be signed 32-bit interleaved PCM. The FFmpeg shim output is expected to be signed 32-bit interleaved PCM.
This keeps the decoder interface suitable for a playback pipeline that This keeps the decoder interface suitable for a playback pipeline that
+3 -3
View File
@@ -73,7 +73,7 @@ use. If a future FFmpeg major release changes a layout before one of the
fields read by this module, the supported range should be extended only after fields read by this module, the supported range should be extended only after
the affected partial definitions have been checked. the affected partial definitions have been checked.
@section{Implementation strategy} @section[#:tag "ffmpeg-definitions-implementation-strategy"]{Implementation strategy}
This module talks directly to the FFmpeg shared libraries through Racket's FFI. This module talks directly to the FFmpeg shared libraries through Racket's FFI.
There is no C shim that hides FFmpeg's structs or normalizes their layout. The There is no C shim that hides FFmpeg's structs or normalizes their layout. The
@@ -90,7 +90,7 @@ Small and stable structures, such as @tt{AVRational} and
then calculates the correct field offsets for the current platform ABI and then calculates the correct field offsets for the current platform ABI and
creates the corresponding pointer type, constructor, accessors and mutators. creates the corresponding pointer type, constructor, accessors and mutators.
The larger FFmpeg structures are handled by @racket[def-cstruct] from The larger FFmpeg structures are handled by @tt{def-cstruct} from
@filepath{private/cstruct-helper.rkt}. Structures such as @filepath{private/cstruct-helper.rkt}. Structures such as
@tt{AVCodecParameters}, @tt{AVStream}, @tt{AVFormatContext}, @tt{AVFrame} and @tt{AVCodecParameters}, @tt{AVStream}, @tt{AVFormatContext}, @tt{AVFrame} and
@tt{AVPacket} are large and may differ between FFmpeg major versions. The @tt{AVPacket} are large and may differ between FFmpeg major versions. The
@@ -329,7 +329,7 @@ audio, one sample frame contains one sample for the left channel and one sample
for the right channel. for the right channel.
} }
@section{Seeking} @section[#:tag "ffmpeg-definitions-seeking"]{Seeking}
@defproc[(fmpg-seek-ms! [instance any/c] @defproc[(fmpg-seek-ms! [instance any/c]
[target-pos-ms exact-nonnegative-integer?]) [target-pos-ms exact-nonnegative-integer?])
+441
View File
@@ -0,0 +1,441 @@
#lang scribble/manual
@(require (for-label racket/base
(except-in racket/contract ->)
racket/path
ffi/unsafe
let-assert
early-return
"../ffmpeg-definitions.rkt"
"../private/cstruct-helper.rkt"))
@title[#:tag "ffmpeg-definitions"]{FFmpeg Decoder Definitions}
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
@defmodule[racket-audio/ffmpeg-definitions]
This module provides the direct FFmpeg-backed decoder layer used by the audio
pipeline. It is deliberately small and stateful. A caller creates one decoder
instance, opens one file on it, queries the selected audio stream, repeatedly
asks for the next PCM block, and closes the instance again.
The module does not expose FFmpeg metadata. It only exposes the information
needed for playback: stream count, sample rate, channel count, duration,
bitrate, decoded PCM data, and sample positions. The output format is fixed:
interleaved signed 32-bit PCM, four bytes per sample, using FFmpeg's
@tt{AV_SAMPLE_FMT_S32} sample format.
The FFmpeg libraries are loaded when the module is required. The module checks
that the runtime FFmpeg major versions are in the supported range configured by
the implementation. This binding targets the FFmpeg library major versions
used by FFmpeg 6, 7, and 8: @tt{libavutil} 58 to 60, @tt{libavcodec} 60 to 62,
@tt{libavformat} 60 to 62, and @tt{libswresample} 4 to 6. Unsupported runtime
versions fail early, before a decoder instance is used.
On Windows, the private library loader may download the bundled sound-library
set into Racket's add-on directory before the FFI libraries are opened. On
Unix-like systems, the FFmpeg libraries are expected to be installed by the
operating system or platform package manager and to be reachable by Racket's
FFI library search path.
@section{Layering}
This module is the low-level Racket FFI layer. It is normally wrapped by
@filepath{ffmpeg-ffi.rkt} and then by @filepath{ffmpeg-decoder.rkt}. The first
wrapper adapts this module to the command protocol used by the audio decoder
frontend. The second wrapper exposes the callback-oriented decoder interface
used by the rest of the playback pipeline.
The distinction matters for buffer lifetime. At this level,
@racket[fmpg-buffer] returns the current buffer owned by the decoder instance.
The adapter in @filepath{ffmpeg-ffi.rkt} copies that buffer before passing it to
@filepath{ffmpeg-decoder.rkt}. Code that uses this module directly must copy
the buffer itself when the bytes must survive the next decoder operation.
@section{FFmpeg version information}
@defproc[(ffmpeg-version [lib (or/c 'avutil 'avcodec 'avformat
'swr 'swresample)])
(list/c exact-nonnegative-integer?
exact-nonnegative-integer?
exact-nonnegative-integer?)]{
Returns the runtime version of one FFmpeg library as a three-element list
containing the major, minor, and micro version numbers. The symbols
@racket['swr] and @racket['swresample] both refer to @tt{libswresample}.
The version is read from FFmpeg's packed integer value. For example, a runtime
value corresponding to @tt{62.28.100} is returned as @racket['(62 28 100)].
The function raises an exception for an unknown library symbol.
}
The runtime versions determine which partial FFmpeg struct layouts are safe to
use. If a future FFmpeg major release changes a layout before one of the
fields read by this module, the supported range should be extended only after
the affected partial definitions have been checked.
@section{Implementation strategy}
This module talks directly to the FFmpeg shared libraries through Racket's FFI.
There is no C shim that hides FFmpeg's structs or normalizes their layout. The
price of that choice is that the Racket side must know enough of the relevant C
struct layouts to read the fields used by the decoder. The benefit is that the
binding remains a Racket module with direct access to the platform FFmpeg
libraries.
@subsection{C structs and offsets}
Small and stable structures, such as @tt{AVRational} and
@tt{AVChannelLayout}, are described with @racket[define-cstruct]. A
@racket[define-cstruct] form describes the C fields to Racket's FFI. Racket
then calculates the correct field offsets for the current platform ABI and
creates the corresponding pointer type, constructor, accessors and mutators.
The larger FFmpeg structures are handled by @racket[def-cstruct] from
@filepath{private/cstruct-helper.rkt}. Structures such as
@tt{AVCodecParameters}, @tt{AVStream}, @tt{AVFormatContext}, @tt{AVFrame} and
@tt{AVPacket} are large and may differ between FFmpeg major versions. The
decoder only needs a few fields from each one, but those fields must still be
read from their exact native offsets.
The helper solves this by describing the complete field sequence up to the last
field the backend needs. Unnamed entries are used only to advance the offset.
Named entries become generated accessors. Repeated entries such as
@racket[(6 _int)] keep the definition compact while still allowing Racket's FFI
to compute alignment, padding and pointer size correctly. Tail fields after
the last required member are not described.
The right layout is selected when the module is required, after the runtime
FFmpeg major versions have been read from the libraries. For the supported
range, @tt{_AVCodecParameters} uses one layout for @tt{libavcodec} major
version 60 and another for major versions 61 and 62. Likewise,
@tt{_AVFrame} uses one layout for @tt{libavutil} major version 58 and
another for major versions 59 and 60. The other partial structs used by this
module are defined with a single layout across the supported versions.
@subsection{Defensive control flow}
Most FFmpeg calls report ordinary failure through C-style return values or null
pointers. The implementation treats those results as normal control flow. The
@racket[let/assert] form is used for setup paths where each native result must
be checked before the next native call is made. It behaves like a sequential
binding form: each binding can be checked immediately, and a failed check
returns the specified failure value for the whole form.
That style is used for opening a file, selecting stream information, allocating
the codec context, and initializing the resampler. Predicates such as
@tt{a-!nullptr?}, @tt{a-nullptr?}, @tt{a-true?}, and @tt{a->=?} express the
usual FFmpeg checks directly next to the binding that produced the value.
The decode and seek paths also use @racket[early-return] where processing must
stop immediately from a nested position. This keeps the normal FFmpeg outcomes
away from exception-based control flow while still making cleanup actions local
to the point where a failure can occur.
@section{Decoder instances}
A decoder instance is an opaque value returned by @racket[fmpg-init]. Its
structure type and predicate are not exported. Pass the value back to the
functions in this module and do not inspect it directly. The contracts below
therefore use @racket[any/c] for the instance argument. Operationally, that
argument must be a value returned by @racket[fmpg-init].
The instance owns native FFmpeg resources: a format context, a codec context,
an audio frame, a resampler, and the Racket byte string used for the current
PCM block. Finalizers are installed as a last line of defence, but callers
should still call @racket[fmpg-close!] explicitly when playback stops or when
the file is no longer needed. Explicit close keeps the lifetime of native
resources predictable.
@defproc[(fmpg-init) any/c]{
Creates a new decoder instance. The result is an opaque instance value, or
@racket[#f] if the instance could not be created.
Creating the instance does not open a file. Use @racket[fmpg-open-file!]
before querying stream information or decoding audio.
}
@defproc[(fmpg-open-file! [instance any/c]
[filename (or/c path? string?)])
(integer-in 0 1)]{
Opens @racket[filename] on @racket[instance], reads the stream information,
selects the best audio stream, initializes the codec context, and initializes
the resampler.
The function returns @racket[1] on success and @racket[0] on failure. On
failure, partially initialized native state is closed again. A non-string,
non-path filename is treated as an open failure and returns @racket[0].
An instance can only have one file open. Close it with @racket[fmpg-close!]
before opening another file on the same instance.
}
@defproc[(fmpg-close! [instance any/c]) void?]{
Closes @racket[instance] if it is open and releases the native FFmpeg resources
owned by the instance. The codec context, frame and resampler are freed before
the format context is closed. This order avoids keeping decoder pointers that
refer to streams from an already closed container.
The stored audio information is reset. Calling this function with @racket[#f]
or with an already closed instance is harmless.
}
@defproc[(fmpg-is-open [instance any/c]) (integer-in 0 1)]{
Returns @racket[1] when @racket[instance] is ready for decoding and @racket[0]
otherwise. An instance is ready only after a file has been opened, a usable
audio stream has been selected, and the decoder and resampler have been
initialized.
}
@section{Audio stream information}
The decoder selects one audio stream for playback using FFmpeg's best-stream
selection. The stream count reports how many audio streams were found in the
container, but decoding is performed only for the selected stream.
The term @italic{sample} in this module means a sample frame: one time step in
the audio stream, across all channels. For stereo 32-bit output, one sample
frame therefore occupies @racket[(* 2 4)] bytes in the returned PCM buffer.
@defproc[(fmpg-audio-stream-count [instance any/c])
exact-nonnegative-integer?]{
Returns the number of audio streams in the open container. If the instance is
not open, the result is @racket[0]. This count is informational; actual stream
selection is performed during @racket[fmpg-open-file!].
}
@deftogether[
(@defproc[(fmpg-audio-sample-rate [instance any/c])
exact-nonnegative-integer?]
@defproc[(fmpg-audio-channels [instance any/c])
exact-nonnegative-integer?])]{
Return the sample rate and channel count of the selected audio stream. If the
instance is not ready, both functions return @racket[0].
}
@deftogether[
(@defproc[(fmpg-audio-bits-per-sample [instance any/c])
exact-positive-integer?]
@defproc[(fmpg-audio-bytes-per-sample [instance any/c])
exact-positive-integer?])]{
Return the fixed output sample width in bits and bytes. The current output
format is 32-bit signed PCM, so @racket[fmpg-audio-bits-per-sample] returns
@racket[32] and @racket[fmpg-audio-bytes-per-sample] returns @racket[4]. The
values are independent of the input file's original sample format and do not
depend on the instance state.
}
@deftogether[
(@defproc[(fmpg-duration-ms [instance any/c]) exact-integer?]
@defproc[(fmpg-duration-samples [instance any/c]) exact-integer?])]{
Return the duration of the selected audio stream in milliseconds and in sample
frames. If the stream duration is not available, the container duration is
used as a fallback. If no duration can be determined, or when the instance is
not ready, the result is @racket[-1].
}
@defproc[(fmpg-file-bitrate [instance any/c]) exact-integer?]{
Returns the container bitrate in bits per second. If the bitrate is unavailable
or if the instance is not open, the result is @racket[-1]. Only positive
FFmpeg bitrates are passed through as reliable.
}
@section{Output format}
The decoder output format is intentionally fixed:
@itemlist[
#:style 'compact
@item{sample format: signed 32-bit PCM, @tt{AV_SAMPLE_FMT_S32}}
@item{layout: interleaved}
@item{sample rate: the selected stream's sample rate}
@item{channels: the selected stream's channel count}
]
This keeps the playback layer simple. The FFmpeg input format may be planar,
floating point, compressed, or otherwise different; @tt{libswresample} converts
the decoded frames to the fixed output format before the bytes are exposed to
Racket.
@section{Decoding}
Decoding is block oriented. Each call to @racket[fmpg-decode-next!] clears the
previous PCM block and attempts to produce the next decoded block for the
selected audio stream. When the call returns @racket[1], the block can be read
with @racket[fmpg-buffer] and described with the buffer query functions.
@defproc[(fmpg-decode-next! [instance any/c]) exact-integer?]{
Decodes until a block of PCM output is available, end of stream is reached, or
an error occurs. The return values are:
@itemlist[
#:style 'compact
@item{@racket[1]: a new PCM buffer is available through @racket[fmpg-buffer].}
@item{@racket[0]: decoding is complete and no more PCM is available.}
@item{A negative value: decoding failed or the instance was not ready.}
]
Internally, the decoder first tries to receive frames that FFmpeg may already
have buffered. If no frame is ready, it reads packets until it finds a packet
for the selected audio stream. Packets from other streams are skipped and
immediately unreferenced. Sent packets are unreferenced after
@tt{avcodec_send_packet}, because the codec has then taken what it needs.
At end of input, the function drains both the codec and the resampler. This is
necessary because FFmpeg and @tt{libswresample} may still hold delayed samples
even after the demuxer has no more packets.
}
@section{Decoded buffers}
The PCM buffer belongs to the decoder instance. It is replaced by the next
call to @racket[fmpg-decode-next!], @racket[fmpg-seek-ms!], or
@racket[fmpg-close!]. Treat the returned byte string as read-only. Copy it if
it must outlive the next decoder operation or if another component may mutate
it.
@defproc[(fmpg-buffer [instance any/c]) (or/c bytes? #f)]{
Returns the current decoded PCM block as a byte string, or @racket[#f] when no
PCM block is available.
The byte string contains interleaved signed 32-bit samples. Its logical frame
count is available as the difference between @racket[fmpg-buffer-end-sample]
and @racket[fmpg-buffer-start-sample]. Its byte size is also available through
@racket[fmpg-buffer-size].
}
@defproc[(fmpg-buffer-size [instance any/c]) exact-nonnegative-integer?]{
Returns the number of valid bytes in the current PCM buffer. If no decoder
state is available, or if the size would not fit in the internal integer range,
the function returns @racket[0].
}
@deftogether[
(@defproc[(fmpg-buffer-start-sample [instance any/c])
exact-nonnegative-integer?]
@defproc[(fmpg-buffer-end-sample [instance any/c])
exact-nonnegative-integer?]
@defproc[(fmpg-sample-position [instance any/c])
exact-nonnegative-integer?])]{
Return sample-frame positions for the current decoder state.
@racket[fmpg-buffer-start-sample] returns the first sample frame represented by
the current PCM buffer. @racket[fmpg-buffer-end-sample] returns the half-open
end position: the first sample frame after the current buffer.
@racket[fmpg-sample-position] returns the next sample position the decoder
expects to produce.
These values count sample frames, not individual channel samples. For stereo
audio, one sample frame contains one sample for the left channel and one sample
for the right channel.
}
@section{Seeking}
@defproc[(fmpg-seek-ms! [instance any/c]
[target-pos-ms exact-nonnegative-integer?])
(integer-in 0 1)]{
Seeks the selected audio stream to @racket[target-pos-ms] milliseconds and
resets the decoder and resampler state. The function returns @racket[1] on
success and @racket[0] on failure. Seeking is allowed only when the instance
is already ready for decoding and the target position is non-negative.
Seeking uses FFmpeg's backward seek flag. FFmpeg may therefore seek to a packet
position before the requested target. The decoder stores a discard target in
sample frames. During the following decode calls, frames before the target are
dropped, and frames that overlap the target are trimmed so the exposed PCM
buffer starts at, or as close as FFmpeg can provide to, the requested position.
After a successful seek, the codec buffers are flushed, the resampler is closed
and reinitialized, EOF state is cleared, and sample bookkeeping is reset to the
target position.
}
@section{Resource ownership}
The decoder instance owns the native FFmpeg objects it allocates. The codec
pointer returned by FFmpeg is not owned by the instance, but the codec context,
frame, resampler and format context are. They are released by
@racket[fmpg-close!]. Finalizers are registered as a safety net, but callers
should close decoder instances explicitly.
Temporary native buffers used during resampling are allocated only for the
duration of a conversion step and are always freed before control returns to the
caller. The public PCM buffer is a Racket byte string, so it can safely be
passed to the Racket-side playback backend.
@section{Use through the decoder frontend}
The direct API above is normally wrapped by @filepath{ffmpeg-ffi.rkt} and by
@filepath{ffmpeg-decoder.rkt}. The frontend function @tt{ffmpeg-open} returns
a handle or @racket[#f] when the file does not exist. Its stream-info callback
receives a mutable hash with at least these playback keys:
@racketblock[
(list 'sample-rate
'channels
'bits-per-sample
'bytes-per-sample
'total-samples
'duration)]
The audio callback receives the same hash extended for the current buffer with
these keys:
@racketblock[
(list 'sample
'current-time)]
The hash is followed by a copied byte string and its valid byte count. The
copy is made by @filepath{ffmpeg-ffi.rkt}, not by the low-level buffer function
itself.
The frontend's seek function accepts a percentage of the stream and translates
that percentage to a sample position. The adapter then translates the sample
position to milliseconds and calls @racket[fmpg-seek-ms!]. This is why the
low-level module exposes millisecond seeking while the frontend exposes
percentage seeking.
@section{Examples}
The following example opens a file, decodes all PCM blocks, and reports their
byte ranges and sample ranges. A real playback loop would pass each buffer to
the audio output layer before requesting the next block.
@racketblock[
(define dec (fmpg-init))
(when (and dec (= (fmpg-open-file! dec "track.ogg") 1))
(printf "~a Hz, ~a channels, ~a ms\n"
(fmpg-audio-sample-rate dec)
(fmpg-audio-channels dec)
(fmpg-duration-ms dec))
(let loop ()
(case (fmpg-decode-next! dec)
[(1)
(define pcm (fmpg-buffer dec))
(define size (fmpg-buffer-size dec))
(define start (fmpg-buffer-start-sample dec))
(define end (fmpg-buffer-end-sample dec))
(printf "decoded ~a bytes, samples [~a, ~a)\n"
size start end)
;; Pass pcm to the audio output layer here, or copy it if needed.
(loop)]
[(0)
(printf "done\n")]
[else
(error "decode error")]))
(fmpg-close! dec))
]
A simple seek flow looks the same after the seek succeeds. The following code
moves to 30 seconds and then requests the next decoded buffer.
@racketblock[
(when (= (fmpg-seek-ms! dec 30000) 1)
(when (= (fmpg-decode-next! dec) 1)
(define pcm (fmpg-buffer dec))
(define start (fmpg-buffer-start-sample dec))
(printf "first buffer after seek starts at sample ~a\n" start)))
]
+1 -1
View File
@@ -105,7 +105,7 @@ When the stream ends, the callback is called as:
The command returns @racket[#t]. The command returns @racket[#t].
@section{Seeking} @section[#:tag "ffmpeg-ffi-seeking"]{Seeking}
The @racket['seek] command takes an absolute PCM sample position: The @racket['seek] command takes an absolute PCM sample position:
+13 -6
View File
@@ -9,7 +9,7 @@
@title{flac-decoder} @title{flac-decoder}
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]] @author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
@defmodule[racket-audio/flac-decoder] @defmodule[racket-audio/flac-decoder #:use-sources (racket-audio/flac-definitions)]
This module provides a small decoder interface on top of the FLAC This module provides a small decoder interface on top of the FLAC
FFI layer. It opens a decoder for a file, reads stream metadata, FFI layer. It opens a decoder for a file, reads stream metadata,
@@ -33,12 +33,12 @@ exist, the result is @racket[#f].
Otherwise a native decoder handler is created with Otherwise a native decoder handler is created with
@racket[flac-ffi-decoder-handler], initialized with the file, and @racket[flac-ffi-decoder-handler], initialized with the file, and
wrapped in a @racket[flac-handle]. The given callbacks are stored wrapped in a @racket[flac-handle?]. The given callbacks are stored
in the handle. in the handle.
When metadata of type @racket['streaminfo] is processed and When metadata of type @racket['streaminfo] is processed and
@racket[cb-stream-info] is a procedure, it is called with a @racket[cb-stream-info] is a procedure, it is called with a
@racket[flac-stream-info] value. @racket[flac-stream-info?] value.
When decoded audio data is processed and @racket[cb-audio] is a When decoded audio data is processed and @racket[cb-audio] is a
procedure, it is called as procedure, it is called as
@@ -80,7 +80,7 @@ with @racket['stopped-reading] and @racket[reading] is reset to
Whenever pending metadata is available, it is processed with Whenever pending metadata is available, it is processed with
@racket[process-meta]. For metadata of type @racket[process-meta]. For metadata of type
@racket['streaminfo], a @racket[flac-stream-info] value is @racket['streaminfo], a @racket[flac-stream-info?] value is
constructed, stored in the handle, and passed to the constructed, stored in the handle, and passed to the
stream-info callback. stream-info callback.
@@ -110,7 +110,7 @@ metadata is processed and the stored stream info is returned.
Otherwise the result is @racket[#f]. Otherwise the result is @racket[#f].
Only metadata of type @racket['streaminfo] is converted into a Only metadata of type @racket['streaminfo] is converted into a
@racket[flac-stream-info] value by this module. @racket[flac-stream-info?] value by this module.
} }
@defproc[(flac-stop [handle flac-handle?]) void?]{ @defproc[(flac-stop [handle flac-handle?]) void?]{
@@ -125,6 +125,13 @@ The procedure prints timing information before and after the
wait. wait.
} }
@defproc[(flac-duration [handle flac-handle?])
(or/c #f exact-nonnegative-integer?)]{
Returns the rounded duration in seconds, or @racket[#f] while no stream
information is available.
}
@section{Diagnostic bindings} @section{Diagnostic bindings}
@defthing[kinds hash?]{ @defthing[kinds hash?]{
@@ -145,7 +152,7 @@ processing.
The block size of the most recently processed frame. The block size of the most recently processed frame.
} }
@section{Notes} @section[#:tag "flac-decoder-notes"]{Notes}
The frame-header hash passed to the audio callback is produced The frame-header hash passed to the audio callback is produced
by @racket[flac-ffi-frame-header]. In this module it is extended by @racket[flac-ffi-frame-header]. In this module it is extended
+2
View File
@@ -27,6 +27,8 @@
@title{@elem{Introduction racket-audio}} @title{@elem{Introduction racket-audio}}
@defmodule[racket-audio]
@;;title{racket-audio} @;;title{racket-audio}
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]] @author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
+3 -3
View File
@@ -22,7 +22,7 @@ buffers together with playback position information, and lets a Racket worker
thread feed libao. Higher-level player code should normally use the public thread feed libao. Higher-level player code should normally use the public
player interface instead of calling this module directly. player interface instead of calling this module directly.
@section{Overview} @section[#:tag "libao-async-overview"]{Overview}
The backend accepts decoded PCM buffers, converts them when needed, groups small The backend accepts decoded PCM buffers, converts them when needed, groups small
buffers into larger playback chunks, and sends those chunks to libao from a buffers into larger playback chunks, and sends those chunks to libao from a
@@ -195,7 +195,7 @@ larger queue elements. The target chunk size is controlled by
different @racket[music-id] values are not merged into the same output chunk. different @racket[music-id] values are not merged into the same output chunk.
} }
@section{Playback state} @section[#:tag "libao-async-playback-state"]{Playback state}
@defproc[(ao_is_at_second_async [handle any/c]) real?]{ @defproc[(ao_is_at_second_async [handle any/c]) real?]{
Returns the playback position, in seconds, associated with the queue element Returns the playback position, in seconds, associated with the queue element
@@ -282,7 +282,7 @@ latency but increase scheduling pressure on the Racket worker thread and on the
audio backend. audio backend.
} }
@section{Implementation strategy} @section[#:tag "libao-async-implementation-strategy"]{Implementation strategy}
The module keeps libao as the only native audio backend, but moves the async The module keeps libao as the only native audio backend, but moves the async
queue and playback thread from C to Racket. It initializes libao lazily when queue and playback thread from C to Racket. It initializes libao lazily when
+306
View File
@@ -0,0 +1,306 @@
#lang scribble/manual
@(require (for-label racket/base
racket/contract
"../libao-async-ffi-racket.rkt"))
@title{Pure Racket Asynchronous libao Backend}
@defmodule[racket-audio/libao-async-ffi-racket]
This module implements the asynchronous libao playback backend used by
@racketmodname[racket-audio]. It provides the same public Racket API as the
older C-backed asynchronous player, but keeps the queueing, buffering,
conversion and worker-thread logic in Racket. The only foreign calls made by
this module are the direct calls into Xiph's libao library.
The module is intended as a low-level backend. Higher-level player code should
normally use the public audio-player interface instead of calling this module
directly. It is documented here because it defines the exact contract between
decoded PCM data and the libao output path.
@section{Overview}
The backend accepts decoded PCM buffers, converts them when needed, groups small
buffers into larger playback chunks, and sends those chunks to libao from a
dedicated Racket worker thread. The worker thread calls @racket[ao_play] as a
blocking foreign call, so other Racket threads and places do not have to wait
for the audio device to accept more data.
Incoming buffers may be interleaved or planar. Planar buffers, such as those
commonly produced by a FLAC decoder, are converted to interleaved PCM before
playback. If the requested sample width cannot be opened on the selected audio
device, the backend tries lower-width output formats and converts samples before
they are sent to libao.
The backend also maintains playback position metadata. Each queued buffer is
tagged with a music id, a current playback position and a duration. These
values are used by the higher-level player to report where the audio device is
in the current track.
@section{Buffer information}
@defproc[(make-buffer-info [type symbol?]
[sample-bits exact-positive-integer?]
[sample-rate exact-positive-integer?]
[channels exact-positive-integer?]
[endianness symbol?])
any/c]{
Creates a buffer description object for PCM data passed to
@racket[ao_play_async].
The @racket[type] field describes the memory layout. The supported values are
@racket['interleaved] for normal interleaved PCM and @racket['planar] for planar
PCM. For compatibility with older code, @racket['ao] is treated as interleaved
by convention and @racket['flac] is accepted as planar input.
The @racket[sample-bits], @racket[sample-rate] and @racket[channels] fields
describe the format of the supplied buffer, not necessarily the format that will
eventually be accepted by the device. The backend may convert the sample width
to the actual device width.
The @racket[endianness] field must be one of @racket['little-endian],
@racket['big-endian] or @racket['native-endian]. It is used when samples are
converted between different sample widths or byte orders.}
@defproc[(make-BufferInfo_t [type symbol?]
[sample-bits exact-positive-integer?]
[sample-rate exact-positive-integer?]
[channels exact-positive-integer?]
[endianness symbol?])
any/c]{
Compatibility alias for @racket[make-buffer-info]. The name matches the older
FFI module and the former C structure naming convention.}
@section{Creating and closing a backend}
@defproc[(ao_version_async) exact-integer?]{
Returns the version number of this asynchronous backend implementation. The
current implementation returns @racket[3]. The value is useful for diagnostics
when multiple asynchronous backend implementations exist.}
@defproc[(ao_create_async [bits exact-positive-integer?]
[rate exact-positive-integer?]
[channels exact-positive-integer?]
[byte-format symbol?]
[wav-output-file (or/c #f path-string?)])
any/c]{
Opens a libao output device and creates an asynchronous playback handle.
The @racket[bits], @racket[rate], @racket[channels] and @racket[byte-format]
arguments describe the preferred output format. The byte format must be one of
@racket['little-endian], @racket['big-endian] or @racket['native-endian].
When @racket[wav-output-file] is @racket[#f], the default live libao driver is
used. When it is a path string, the backend opens libao's @tt{wav} driver and
writes the audio stream to that file instead.
The backend first tries to open the requested sample width. If that fails and
the requested width is greater than 24 bits, it tries 24-bit output. If that
also fails and the requested width is greater than 16 bits, it tries 16-bit
output. The actual device width can be queried with
@racket[ao_real_output_bits_async].
The function returns a playback handle on success and @racket[#f] when no
suitable libao device could be opened.}
@defproc[(ao_stop_async [handle any/c]) any/c]{
Stops the worker thread, clears pending audio, closes the libao device and
invalidates @racket[handle].
The stop operation first clears all queued buffers, then queues an internal stop
command, waits for the playback thread to terminate, and finally closes the
underlying libao handle. Calling this function on an already invalid handle is
an error.}
@section{Submitting audio}
@defproc[(ao_play_async [handle any/c]
[music-id any/c]
[at-second real?]
[music-duration real?]
[buf-size exact-nonnegative-integer?]
[au-buf (or/c bytes? any/c)]
[info any/c])
void?]{
Queues a PCM buffer for asynchronous playback.
The @racket[music-id], @racket[at-second] and @racket[music-duration] values are
stored together with the queued buffer. They do not affect sample conversion,
but they allow the player to report the current track id, playback position and
track duration while the worker thread is playing the queued data.
The @racket[buf-size] argument gives the number of valid bytes in
@racket[au-buf]. The input buffer is copied into backend-owned memory before
the function returns, so the caller may reuse or discard the original byte
string after the call.
The @racket[info] argument should be created with @racket[make-buffer-info]. If
the buffer is planar, it is converted to interleaved PCM. If the buffer's
sample width or byte order differs from the actual libao device format, the
backend converts it before queueing.
The backend groups smaller buffers into larger playback chunks. This reduces
the number of calls to libao and helps prevent underruns. Buffers with
different @racket[music-id] values are not merged into the same output chunk.}
@defproc[(ao_clear_async [handle any/c]) any/c]{
Clears all queued audio buffers that have not yet been played.
The current aggregation buffer is also cleared. Already playing audio may still
finish at the device level, depending on what libao and the operating system
have accepted. This operation is used by higher-level code when stopping,
seeking or replacing the current stream.}
@section{Playback state}
@defproc[(ao_is_at_second_async [handle any/c]) real?]{
Returns the playback position associated with the most recently dequeued buffer.
This value is the @racket[at-second] value supplied to @racket[ao_play_async],
not a sample-accurate query into the audio device.}
@defproc[(ao_is_at_music_id_async [handle any/c]) any/c]{
Returns the music id associated with the most recently dequeued buffer. The
higher-level player uses this value to determine which track the output thread
has reached.}
@defproc[(ao_music_duration_async [handle any/c]) real?]{
Returns the duration associated with the most recently dequeued buffer. This is
the @racket[music-duration] value supplied to @racket[ao_play_async].}
@defproc[(ao_bufsize_async [handle any/c]) exact-nonnegative-integer?]{
Returns the number of queued PCM bytes that have been accepted by the backend
but not yet removed from the asynchronous queue. This is a backend queue size,
not the size of the operating-system or hardware audio buffer.}
@defproc[(ao_sample_queue_len [handle any/c]) exact-nonnegative-integer?]{
Returns the number of queued playback elements waiting in the backend queue.
This is mainly useful for diagnostics and tuning.}
@defproc[(ao_reuse_buf_len [handle any/c]) exact-nonnegative-integer?]{
Returns the number of reusable internal buffers currently kept by the backend.
This is a diagnostic value that can help detect excessive allocation or
unexpected buffer retention.}
@section{Pause and volume}
@defproc[(ao_pause_async [handle any/c]
[paused (or/c boolean? integer?)])
void?]{
Pauses or resumes the playback worker.
When @racket[paused] is @racket[#t], or an integer other than @racket[0], the
worker thread is blocked before it dequeues the next element. When
@racket[paused] is @racket[#f] or @racket[0], playback is resumed.
Pausing does not prevent producers from queueing additional buffers. It only
prevents the worker thread from taking more data from the queue.}
@defproc[(ao_set_volume_async [handle any/c]
[percentage real?])
void?]{
Sets the output volume as a percentage.
A value of @racket[100.0] means unchanged volume. Values below
@racket[100.0] attenuate the signal. Values above @racket[100.0] amplify the
signal and are clipped to the signed range of the actual device sample width.
Internally the value is stored as an integer in hundredths of a percent: for
example, @racket[100.0] becomes @racket[10000]. Values very close to
@racket[100.0] are normalized to exactly @racket[10000] to avoid unnecessary
sample processing.}
@defproc[(ao_volume_async [handle any/c]) real?]{
Returns the currently configured output volume percentage.}
@section{Output format}
@defproc[(ao_real_output_bits_async [handle any/c])
exact-nonnegative-integer?]{
Returns the actual sample width opened on the libao device.
This may be lower than the requested width passed to @racket[ao_create_async].
For example, a request for 32-bit output may result in a 24-bit or 16-bit device
when the default libao driver cannot open the preferred format. In that case,
@racket[ao_play_async] converts the incoming samples before playback.}
@section{Playback buffer tuning}
@defproc[(ao-playback-buf-ms) exact-nonnegative-integer?]{
Returns the target size, in milliseconds, of the playback chunks that the
backend sends to libao. The default is @racket[150].}
@defproc[(ao-set-playback-buf-ms! [ms exact-nonnegative-integer?])
void?]{
Sets the target playback chunk size in milliseconds.
Larger values reduce the number of calls to libao and may help prevent audible
glitches when decoders produce many small buffers. Smaller values reduce
latency but increase scheduling pressure on the Racket worker thread and on the
audio backend.}
@section{Implementation notes}
The worker thread is created with its own thread pool and uses libao's
@racket[ao_play] through a blocking FFI call. Before calling libao, the worker
copies the queued bytes into memory allocated with @racket['atomic-interior].
This is important because a blocking foreign call must not be handed a pointer
to movable Racket memory that could be relocated by the garbage collector while
the foreign function is still using it.
The backend keeps a small pool of previously allocated buffers. Buffers created
internally for conversion or aggregation can be reused after playback. This
reduces allocation pressure during continuous playback.
The module initializes libao when the first handle is opened and shuts libao
down when the last handle is closed. This keeps libao lifetime management local
to the backend and avoids repeated global initialization during normal playback.
@section{Example}
@racketblock[
(define h
(ao_create_async 32 44100 2 'native-endian #f))
(define info
(make-buffer-info 'interleaved 32 44100 2 'native-endian))
(when h
(ao_play_async h
1
0.0
180.0
(bytes-length pcm-bytes)
pcm-bytes
info)
(ao_set_volume_async h 80.0)
(ao_pause_async h #t)
(ao_pause_async h #f)
(ao_stop_async h))
]
The example opens the default live libao device, queues one interleaved
32-bit PCM buffer, lowers the volume to 80 percent, briefly pauses and resumes
the worker, and finally closes the backend.
+3 -3
View File
@@ -23,7 +23,7 @@ stores the requested playback configuration together with a native
asynchronous player handle. It also records the real bit depth accepted asynchronous player handle. It also records the real bit depth accepted
by the selected libao output device. by the selected libao output device.
@section{Audio handles} @section[#:tag "libao-audio-handles"]{Audio handles}
@defproc[(ao-handle? [v any/c]) boolean?]{ @defproc[(ao-handle? [v any/c]) boolean?]{
@@ -216,7 +216,7 @@ A true value pauses playback. @racket[#f] resumes playback.
Clears buffered asynchronous playback data for @racket[handle]. Clears buffered asynchronous playback data for @racket[handle].
} }
@section{Playback state} @section[#:tag "libao-playback-state"]{Playback state}
@defproc[(ao-at-second [handle ao-handle?]) number?]{ @defproc[(ao-at-second [handle ao-handle?]) number?]{
@@ -259,7 +259,7 @@ Returns the current playback volume as reported by the native
asynchronous player. asynchronous player.
} }
@section{Notes} @section[#:tag "libao-notes"]{Notes}
This module is a higher-level wrapper around the asynchronous FFI layer. This module is a higher-level wrapper around the asynchronous FFI layer.
It stores the playback configuration in the handle, and reuses that It stores the playback configuration in the handle, and reuses that
+8 -8
View File
@@ -16,10 +16,10 @@ reports stream information through a callback, streams decoded PCM
buffers, and supports stopping and seeking. buffers, and supports stopping and seeking.
The module is intended to be used through The module is intended to be used through
@racketmodname[racket-sound/audio-decoder], but its procedures can also @racketmodname[racket-audio/audio-decoder], but its procedures can also
be used directly. be used directly.
@section{Validation} @section[#:tag "mp3-decoder-validation"]{Validation}
@defproc[(mp3-valid? [mp3-file any/c]) boolean?]{ @defproc[(mp3-valid? [mp3-file any/c]) boolean?]{
@@ -27,14 +27,14 @@ Returns #t.
The current implementation does not inspect mp3-file. This procedure The current implementation does not inspect mp3-file. This procedure
exists to satisfy the reader interface used by exists to satisfy the reader interface used by
@racketmodname[racket-sound/audio-decoder]. @racketmodname[racket-audio/audio-decoder].
Basic validation such as file existence and extension matching is Basic validation such as file existence and extension matching is
performed in the higher-level module. This procedure therefore acts as performed in the higher-level module. This procedure therefore acts as
an additional hook and currently accepts all inputs. an additional hook and currently accepts all inputs.
} }
@section{Opening} @section[#:tag "mp3-decoder-opening"]{Opening}
@defproc[(mp3-open [mp3-file* (or/c path? string?)] @defproc[(mp3-open [mp3-file* (or/c path? string?)]
[cb-stream-info procedure?] [cb-stream-info procedure?]
@@ -68,7 +68,7 @@ where info is a mutable hash containing at least:
@item{'total-samples}] @item{'total-samples}]
} }
@section{Reading} @section[#:tag "mp3-decoder-reading"]{Reading}
@defproc[(mp3-read [handle struct?]) any/c]{ @defproc[(mp3-read [handle struct?]) any/c]{
@@ -104,7 +104,7 @@ After termination, the underlying decoder is closed and released.
The return value is otherwise unspecified. The return value is otherwise unspecified.
} }
@section{Seeking} @section[#:tag "mp3-decoder-seeking"]{Seeking}
@defproc[(mp3-seek [handle struct?] @defproc[(mp3-seek [handle struct?]
[percentage number?]) [percentage number?])
@@ -127,7 +127,7 @@ If the total number of samples is unavailable or equal to -1, this
procedure has no effect. procedure has no effect.
} }
@section{Stopping} @section[#:tag "mp3-decoder-stopping"]{Stopping}
@defproc[(mp3-stop [handle struct?]) void?]{ @defproc[(mp3-stop [handle struct?]) void?]{
@@ -137,7 +137,7 @@ The procedure sets an internal stop flag and waits until the read loop
has terminated, sleeping briefly between checks. has terminated, sleeping briefly between checks.
} }
@section{Notes} @section[#:tag "mp3-decoder-notes"]{Notes}
The stream-info hash is shared between initialization and decoding and The stream-info hash is shared between initialization and decoding and
is updated in place during playback. is updated in place during playback.
+196
View File
@@ -0,0 +1,196 @@
#lang scribble/manual
@(require racket/base
(for-label racket/base
racket/contract
racket/path
"../opusfile-decoder.rkt"))
@title{opusfile-decoder}
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
@defmodule[racket-audio/opusfile-decoder]
This module provides an Opus decoder backend based on Xiph
@tt{libopusfile}. It opens Ogg Opus files, reports stream information
through a callback, streams decoded interleaved PCM buffers, and supports
stopping and seeking.
The module is intended to be used through
@racketmodname[racket-audio/audio-decoder], but its procedures can also
be used directly.
Opus decoding produces 48 kHz PCM. The original input rate stored in an
Opus file, if present, is not the decoder output sample rate.
@section{Availability}
@defproc[(opusfile-available?) boolean?]{
Returns @racket[#t] if @tt{libopusfile} and the native procedures used
by this backend could be loaded, and @racket[#f] otherwise.
The generic @racketmodname[racket-audio/audio-decoder] module prefers
this decoder for Opus streams when it is available. If it is not
available, Opus files may still be handled by the FFmpeg backend if that
backend is available.
}
@section{Output format setting}
@defproc[(opusfile-output-format? [v any/c]) boolean?]{
Returns @racket[#t] when @racket[v] is one of the supported Opus decoder
output formats: @racket['s16] or @racket['s24].
}
@defthing[current-opusfile-output-format procedure?]{
A global output-format setting for this decoder.
Called without arguments, it returns the current output format. Called
with one argument, it sets the current output format and returns the new
value. The accepted values are:
@itemlist[#:style 'compact
@item{@racket['s16] --- signed 16-bit interleaved PCM. This is the
default. The backend uses @tt{op_read}.}
@item{@racket['s24] --- packed signed 24-bit interleaved PCM in native
byte order. The backend uses @tt{op_read_float} and converts the
float samples to 24-bit PCM.}]
The setting is global. It is read when stream format hashes are produced
and when audio buffers are decoded. For normal use, set it before
opening or reading an Opus file.
Example:
@racketblock[
(current-opusfile-output-format 's24)
]
This binding is also re-exported by
@racketmodname[racket-audio/audio-decoder] and by
@racketmodname[racket-audio].
}
@section[#:tag "opusfile-decoder-validation"]{Validation}
@defproc[(opusfile-valid? [audio-file any/c]) boolean?]{
Returns @racket[#t] when @tt{libopusfile} is available and
@racket[audio-file] exists.
This predicate is deliberately small. Detailed validation is performed
when the file is opened by @racket[opusfile-open]. The generic decoder
layer also performs extension and existence checks before opening a file.
}
@section[#:tag "opusfile-decoder-opening"]{Opening}
@defproc[(opusfile-open [audio-file (or/c path? string?)]
[cb-stream-info procedure?]
[cb-audio procedure?])
(or/c struct? #f)]{
Opens @racket[audio-file] with @tt{libopusfile} and returns an opaque
Opus decoder handle. If @racket[audio-file] is a path, it is converted
with @racket[path->string]. If the file does not exist, the result is
@racket[#f].
If @tt{libopusfile} cannot be loaded, an exception is raised. If the
file exists but cannot be opened by @tt{libopusfile}, an exception is
raised with the native Opusfile error code.
The stream-info callback is called once after the file has been opened:
@racketblock[
(cb-stream-info info)
]
where @racket[info] is a mutable hash containing at least:
@itemlist[#:style 'compact
@item{@racket['duration] --- duration in seconds, based on the total
number of decoded PCM samples when available;}
@item{@racket['sample-rate] --- always @racket[48000];}
@item{@racket['channels] --- number of decoded channels;}
@item{@racket['bits-per-sample] --- @racket[16] for @racket['s16] and
@racket[24] for @racket['s24];}
@item{@racket['bytes-per-sample] --- @racket[2] for @racket['s16] and
@racket[3] for @racket['s24];}
@item{@racket['sample-format] --- the value of
@racket[current-opusfile-output-format];}
@item{@racket['total-samples] --- total number of decoded PCM samples,
or the value reported by @tt{libopusfile}.}]
}
@section[#:tag "opusfile-decoder-reading"]{Reading}
@defproc[(opusfile-read [handle struct?]) any/c]{
Starts the decode loop for @racket[handle].
The loop repeatedly decodes audio blocks and invokes the audio callback:
@racketblock[
(cb-audio info buffer size)
]
where @racket[info] is the mutable stream-info hash, @racket[buffer] is
an interleaved PCM buffer, and @racket[size] is the buffer size in bytes.
Before each callback, the info hash is updated in place with:
@itemlist[#:style 'compact
@item{@racket['sample] --- the current decoded sample position;}
@item{@racket['current-time] --- the current decoded time in seconds.}]
The buffer format is determined by
@racket[current-opusfile-output-format]. In @racket['s16] mode the
buffer contains signed 16-bit PCM. In @racket['s24] mode the buffer
contains packed signed 24-bit PCM.
The loop also checks for a pending seek request. If a seek has been
requested with @racket[opusfile-seek], it is applied before the next
read from @tt{libopusfile}.
The loop terminates at end-of-stream or when a stop has been requested
with @racket[opusfile-stop]. After termination, the underlying
@tt{libopusfile} handle is freed.
}
@section[#:tag "opusfile-decoder-seeking"]{Seeking}
@defproc[(opusfile-seek [handle struct?]
[percentage number?])
void?]{
Requests a seek within the stream.
The @racket[percentage] argument represents a position relative to the
full stream, where @racket[0] is the start and @racket[100] is the end.
The value may be fractional and is clamped to that range.
If the total sample count is known and non-zero, the procedure computes
a target decoded sample and stores it as a pending seek request. The
actual native seek is performed later by @racket[opusfile-read].
}
@section[#:tag "opusfile-decoder-stopping"]{Stopping}
@defproc[(opusfile-stop [handle struct?]) void?]{
Requests termination of an active @racket[opusfile-read] loop.
The procedure sets an internal stop flag and waits until the read loop
has terminated, sleeping briefly between checks.
}
@section[#:tag "opusfile-decoder-notes"]{Notes}
The Opusfile backend is registered in
@racketmodname[racket-audio/audio-decoder] as reader type
@racket['opusfile] for files and streams recognized as Opus.
The generic decoder callbacks therefore receive @racket['opusfile] as
@racket[audio-type] and @racket['ao] as @racket[ao-type].
+1 -1
View File
@@ -12,7 +12,7 @@
@defmodule[racket-audio/play-test] @defmodule[racket-audio/play-test]
The @racketmodname[racket-audio/play-test.rkt] module is a small integration test and The @racketmodname[racket-audio/play-test] module is a small integration test and
usage example for @racketmodname[racket-audio/audio-player]. It is not the public usage example for @racketmodname[racket-audio/audio-player]. It is not the public
playback API itself; normal applications should use @racketmodname[racket-audio/audio-player] playback API itself; normal applications should use @racketmodname[racket-audio/audio-player]
directly. This module shows how a program can create an audio player, observe directly. This module shows how a program can create an audio player, observe
+4
View File
@@ -21,11 +21,15 @@
@include-section["audio-player.scrbl"] @include-section["audio-player.scrbl"]
@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["resampler.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"]
@include-section["audio-decoder.scrbl"] @include-section["audio-decoder.scrbl"]
@include-section["libao-async-ffi-racket.scrbl"] @include-section["libao-async-ffi-racket.scrbl"]
@include-section["flac-decoder.scrbl"] @include-section["flac-decoder.scrbl"]
@include-section["opusfile-decoder.scrbl"]
@include-section["mp3-decoder.scrbl"] @include-section["mp3-decoder.scrbl"]
@include-section["ffmpeg-decoder.scrbl"] @include-section["ffmpeg-decoder.scrbl"]
@include-section["libao.scrbl"] @include-section["libao.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.
}
+201 -81
View File
@@ -9,48 +9,72 @@
@title{TagLib Metadata} @title{TagLib Metadata}
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]] @author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
@defmodule[racket-audio/taglib] @defmodule[racket-audio/taglib]
The @racketmodname[racket-audio/taglib] module provides the high level metadata The @racketmodname[racket-audio/taglib] module provides the high level metadata
reader used by the audio package. It wraps the lower level TagLib FFI module API used by the audio package. It wraps the lower level TagLib C FFI module and
and presents a small, read-only Racket API for common tags, audio properties, presents a Racket API for common tags, generic properties, audio properties, and
generic properties, and embedded cover art. embedded cover art.
Calling @racket[id3-tags] opens the file through TagLib, copies the values that The module can be used in two modes. The default mode is read-only and returns
are needed on the Racket side, reads the optional embedded picture, frees the a snapshot of the metadata. A handle opened with @racket[#:mode 'read-write]
native TagLib objects, and returns an opaque tag handle. The handle is keeps the native TagLib file open and can be modified with the setter
therefore a snapshot of the metadata at the time it was read. It does not keep procedures documented below. Changes are written to the media file by calling
the media file or the native TagLib handle open. @racket[tags-save!].
The name @racket[id3-tags] is historical. The module uses TagLib to open the The name @racket[id3-tags] is historical. The implementation uses TagLib, so
file, so the usable file types are the file types supported by the TagLib the usable file types are the file types supported by the TagLib library
library available at run time. This module is not a tag editor; it only reads available at run time.
metadata.
@section{Reading metadata} @section{Opening and closing tag handles}
@defproc[(id3-tags [file path-string?]) any/c]{ @defproc[(id3-tags [file path-string?]
Reads metadata from @racket[file] and returns an opaque tag handle. The [#:mode mode (or/c 'read 'read-only 'read-write 'write) 'read])
argument may be a path or a string. On Windows, the implementation retries any/c]{
with the wide-character TagLib open function when the normal open function does Opens @racket[file] through TagLib and returns an opaque tag handle. In the
not produce a valid TagLib file. default read-only mode, the module copies the values needed on the Racket side,
frees the native TagLib objects, and returns a snapshot handle.
The returned handle is passed to the other procedures in this module. If the In read-write mode, the native TagLib file remains open. Setter procedures may
file cannot be opened, @racket[id3-tags] still returns a handle, but then be used to modify fields, properties, and pictures. Call
@racket[tags-valid?] returns @racket[#f]. Other accessors then return their @racket[tags-save!] to write changes and @racket[tags-close!] to close the
default values, such as @racket[""], @racket[-1], @racket['()], or native handle.
@racket[#f].}
@defproc[(tags-valid? [tags any/c]) boolean?]{ On Windows, the implementation retries with the wide-character TagLib open
Returns @racket[#t] when @racket[id3-tags] successfully opened the file and function when the normal open function does not produce a valid TagLib file.}
TagLib reported it as valid.}
@defproc[(call-with-id3-tags [file path-string?]
[proc procedure?]
[#:mode mode (or/c 'read 'read-only 'read-write 'write) 'read])
any/c]{
Opens @racket[file], calls @racket[proc] with the tag handle, and closes the
handle afterwards with @racket[tags-close!]. This is most useful for
read-write code because it avoids leaking the native TagLib file handle.}
@deftogether[
(@defproc[(tags-valid? [tags any/c]) boolean?]
@defproc[(tags-read-write? [tags any/c]) boolean?]
@defproc[(tags-closed? [tags any/c]) boolean?])]{
Return handle state. @racket[tags-valid?] reports whether TagLib opened the
file successfully. @racket[tags-read-write?] reports whether the handle was
opened in read-write mode. @racket[tags-closed?] reports whether the native
TagLib file handle has been closed.}
@deftogether[
(@defproc[(tags-save! [tags any/c]) boolean?]
@defproc[(tags-close! [tags any/c]) void?])]{
@racket[tags-save!] writes pending changes for a read-write handle to the media
file. @racket[tags-close!] closes the native TagLib file handle. Closing a
read-only snapshot is harmless.
}
@racketblock[ @racketblock[
(define tags (id3-tags "song.mp3")) (call-with-id3-tags "track.flac"
(lambda (tags)
(when (tags-valid? tags) (when (tags-valid? tags)
(printf "~a - ~a\n" (tags-artist tags) (tags-title tags)))] (tags-title! tags "New title")
(tags-save! tags)))
#:mode 'read-write)]
@section{Common tag fields} @section{Common tag fields}
@@ -59,32 +83,52 @@ TagLib reported it as valid.}
@defproc[(tags-album [tags any/c]) string?] @defproc[(tags-album [tags any/c]) string?]
@defproc[(tags-artist [tags any/c]) string?] @defproc[(tags-artist [tags any/c]) string?]
@defproc[(tags-comment [tags any/c]) string?] @defproc[(tags-comment [tags any/c]) string?]
@defproc[(tags-genre [tags any/c]) string?])] @defproc[(tags-genre [tags any/c]) string?])]{
Return the common textual fields from the TagLib tag interface. Missing fields Return the common textual fields from the TagLib tag interface. Missing fields
are returned as the empty string. are returned as the empty string.}
@deftogether[ @deftogether[
(@defproc[(tags-year [tags any/c]) integer?] (@defproc[(tags-year [tags any/c]) integer?]
@defproc[(tags-track [tags any/c]) integer?])] @defproc[(tags-track [tags any/c]) integer?])]{
Return the year and track number from the common TagLib tag interface. Missing Return the year and track number from the common TagLib tag interface. Missing
numeric values are returned as @racket[-1]. numeric values are returned as @racket[-1].}
@deftogether[ @deftogether[
(@defproc[(tags-composer [tags any/c]) (@defproc[(tags-title! [tags any/c] [value (or/c string? 'clear)]) void?]
(or/c string? (listof string?))] @defproc[(tags-album! [tags any/c] [value (or/c string? 'clear)]) void?]
@defproc[(tags-album-artist [tags any/c]) @defproc[(tags-artist! [tags any/c] [value (or/c string? 'clear)]) void?]
(or/c string? (listof string?))] @defproc[(tags-comment! [tags any/c] [value (or/c string? 'clear)]) void?]
@defproc[(tags-disc-number [tags any/c]) @defproc[(tags-genre! [tags any/c] [value (or/c string? 'clear)]) void?])]{
(or/c number? #f)])] Set common textual fields on a read-write handle. Passing @racket['clear]
Return selected values from the generic TagLib property store. The composer is clears the field. Call @racket[tags-save!] to persist the change.}
read from the lower-case @racket['composer] key, the album artist from
@racket['albumartist], and the disc number from @racket['discnumber].
Composer and album artist return a list of strings when the property is present @deftogether[
and the empty string when it is missing. The disc number is parsed from the (@defproc[(tags-year! [tags any/c] [value (or/c exact-nonnegative-integer? 'clear)]) void?]
first property value and defaults to @racket[-1]. If the stored value cannot be @defproc[(tags-track! [tags any/c] [value (or/c exact-nonnegative-integer? 'clear)]) void?])]{
parsed as a number, the result may be @racket[#f]. Use @racket[tags-keys] and Set numeric common fields on a read-write handle. Passing @racket['clear]
@racket[tags-ref] for direct access to the complete generic property store. writes zero through the TagLib C API and updates the Racket-side cache to
@racket[-1].}
@section{Selected generic fields}
@deftogether[
(@defproc[(tags-composer [tags any/c]) (or/c string? (listof string?))]
@defproc[(tags-album-artist [tags any/c]) (or/c string? (listof string?))]
@defproc[(tags-disc-number [tags any/c]) (or/c number? #f)])]{
Return selected values from the generic TagLib property store. The composer is
read from the @racket['composer] key, the album artist from
@racket['albumartist], and the disc number from @racket['discnumber]. Use
@racket[tags-keys] and @racket[tags-ref] for direct access to the complete
generic property store.}
@deftogether[
(@defproc[(tags-composer! [tags any/c] [value (or/c string? 'clear)]) void?]
@defproc[(tags-album-artist! [tags any/c] [value (or/c string? 'clear)]) void?]
@defproc[(tags-disc-number! [tags any/c]
[value (or/c exact-nonnegative-integer? string? 'clear)])
void?])]{
Set selected generic properties on a read-write handle. The disc number may be
provided as a number or as the exact string that should be written.}
@section{Audio properties} @section{Audio properties}
@@ -92,10 +136,11 @@ parsed as a number, the result may be @racket[#f]. Use @racket[tags-keys] and
(@defproc[(tags-length [tags any/c]) integer?] (@defproc[(tags-length [tags any/c]) integer?]
@defproc[(tags-sample-rate [tags any/c]) integer?] @defproc[(tags-sample-rate [tags any/c]) integer?]
@defproc[(tags-bit-rate [tags any/c]) integer?] @defproc[(tags-bit-rate [tags any/c]) integer?]
@defproc[(tags-channels [tags any/c]) integer?])] @defproc[(tags-channels [tags any/c]) integer?])]{
Return audio properties reported by TagLib: length in seconds, sample rate in Return audio properties reported by TagLib: length in seconds, sample rate in
Hz, bit rate in kbit/s, and number of channels. Missing values are returned as Hz, bit rate in kbit/s, and number of channels. These values are read-only
@racket[-1]. properties of the media stream, not editable tags. Missing values are returned
as @racket[-1].}
@section{Generic properties} @section{Generic properties}
@@ -109,9 +154,39 @@ Returns the list of values associated with @racket[key], or @racket[#f] when the
property was not found. Use lower-case symbol keys, matching the values property was not found. Use lower-case symbol keys, matching the values
returned by @racket[tags-keys].} returned by @racket[tags-keys].}
@defproc[(tags-set! [tags any/c]
[key (or/c symbol? string?)]
[value (or/c string? 'clear)])
void?]{
Sets a generic property on a read-write handle. Symbol keys are converted to
upper-case TagLib property names; string keys are passed as supplied. Passing
@racket['clear] clears the property.}
@defproc[(tags-set-values! [tags any/c]
[key (or/c symbol? string?)]
[values (or/c (listof string?) 'clear)])
void?]{
Replaces a generic property with zero or more values. Passing @racket['clear]
removes the property.}
@defproc[(tags-append! [tags any/c]
[key (or/c symbol? string?)]
[value string?])
void?]{
Appends a value to a generic property on a read-write handle.}
@defproc[(tags-clear! [tags any/c]
[key (or/c symbol? string?)])
void?]{
Clears a generic property on a read-write handle.}
@racketblock[ @racketblock[
(for ([key (in-list (tags-keys tags))]) (call-with-id3-tags "track.flac"
(printf "~a: ~s\n" key (tags-ref tags key)))] (lambda (tags)
(tags-set-values! tags 'composer '("Johann Sebastian Bach"))
(tags-set! tags 'discnumber "1")
(tags-save! tags))
#:mode 'read-write)]
Generic properties may contain multiple values for a single key. The API keeps Generic properties may contain multiple values for a single key. The API keeps
those values as lists instead of joining them into one string. those values as lists instead of joining them into one string.
@@ -120,8 +195,8 @@ those values as lists instead of joining them into one string.
The module represents embedded artwork as an opaque @deftech{picture value}. The module represents embedded artwork as an opaque @deftech{picture value}.
The picture value is returned by @racket[tags-picture] and can be inspected with The picture value is returned by @racket[tags-picture] and can be inspected with
the picture procedures documented below. When no picture is available, the the picture procedures documented below. It can also be written to another
picture-related procedures return @racket[#f]. file with @racket[tags-picture!] or @racket[tags-append-picture!].
@defproc[(tags-picture [tags any/c]) (or/c any/c #f)]{ @defproc[(tags-picture [tags any/c]) (or/c any/c #f)]{
Returns the embedded picture value, or @racket[#f] when the file has no picture Returns the embedded picture value, or @racket[#f] when the file has no picture
@@ -130,14 +205,15 @@ that the underlying FFI layer could read.}
@deftogether[ @deftogether[
(@defproc[(tags-picture->kind [tags any/c]) (or/c integer? #f)] (@defproc[(tags-picture->kind [tags any/c]) (or/c integer? #f)]
@defproc[(tags-picture->mimetype [tags any/c]) (or/c string? #f)] @defproc[(tags-picture->mimetype [tags any/c]) (or/c string? #f)]
@defproc[(tags-picture->description [tags any/c]) (or/c string? #f)]
@defproc[(tags-picture->size [tags any/c]) (or/c integer? #f)] @defproc[(tags-picture->size [tags any/c]) (or/c integer? #f)]
@defproc[(tags-picture->ext [tags any/c]) (or/c symbol? #f)])] @defproc[(tags-picture->ext [tags any/c]) (or/c symbol? #f)])]{
Return selected information about the embedded picture. The kind is the Return selected information about the embedded picture. The kind is the
numeric picture type reported by the FFI layer. The MIME type is the stored numeric picture type reported by the FFI layer. The MIME type is the stored
MIME type, such as @racket["image/jpeg"] or @racket["image/png"]. The size is MIME type, such as @racket["image/jpeg"] or @racket["image/png"]. The size is
the number of bytes in the embedded image. The extension helper returns the number of bytes in the embedded image. The extension helper returns
@racket['jpg], @racket['png], or @racket[#f] when the MIME type is not @racket['jpg], @racket['png], or @racket[#f] when the MIME type is not
recognized. recognized.}
@defproc[(tags-picture->bitmap [tags any/c]) @defproc[(tags-picture->bitmap [tags any/c])
(or/c (is-a?/c bitmap%) #f)]{ (or/c (is-a?/c bitmap%) #f)]{
@@ -150,42 +226,86 @@ Reads the embedded picture bytes with @racket[read-bitmap] and returns a
boolean?]{ boolean?]{
Writes the embedded picture bytes to @racket[path] in binary mode, replacing an Writes the embedded picture bytes to @racket[path] in binary mode, replacing an
existing file. The procedure returns @racket[#t] when a picture was written and existing file. The procedure returns @racket[#t] when a picture was written and
@racket[#f] when the tag handle has no picture. The file name is not adjusted @racket[#f] when the tag handle has no picture.}
automatically; use @racket[tags-picture->ext] when the caller wants to choose an
extension from the MIME type.} @defproc[(make-tags-picture [mimetype string?]
[kind integer?]
[data (or/c bytes? (is-a?/c bitmap%))]
[#:description description string? ""])
id3-picture?]{
Creates a picture value from encoded image bytes or from a @racket[bitmap%].
The MIME type should normally be @racket["image/jpeg"] or @racket["image/png"].}
@defproc[(make-tags-picture-from-bitmap [bitmap (is-a?/c bitmap%)]
[kind integer?]
[#:mimetype mimetype string? "image/png"]
[#:description description string? ""])
id3-picture?]{
Creates a picture value by encoding @racket[bitmap] as PNG or JPEG.}
@deftogether[
(@defproc[(tags-picture! [tags any/c]
[picture (or/c id3-picture? 'clear)])
void?]
@defproc[(tags-append-picture! [tags any/c]
[picture id3-picture?])
void?]
@defproc[(tags-clear-picture! [tags any/c]) void?])]{
Set, append, or clear embedded artwork on a read-write handle. The procedures
use TagLib complex properties underneath. Call @racket[tags-save!] to persist
the change.}
@racketblock[ @racketblock[
(define ext (tags-picture->ext tags)) (define cover
(make-tags-picture "image/jpeg" 3 (file->bytes "cover.jpg")
#:description "Cover"))
(when ext (call-with-id3-tags "track.flac"
(tags-picture->file tags (lambda (tags)
(format "cover.~a" ext)))] (tags-picture! tags cover)
(tags-save! tags))
#:mode 'read-write)]
@section{Picture values} @section{Picture values}
@deftogether[ @deftogether[
(@defproc[(id3-picture-mimetype [picture any/c]) string?] (@defproc[(id3-picture? [v any/c]) boolean?]
@defproc[(id3-picture-kind [picture any/c]) integer?] @defproc[(id3-picture-mimetype [picture id3-picture?]) string?]
@defproc[(id3-picture-size [picture any/c]) integer?] @defproc[(id3-picture-kind [picture id3-picture?]) integer?]
@defproc[(id3-picture-bytes [picture any/c]) bytes?])] @defproc[(id3-picture-size [picture id3-picture?]) integer?]
Access the fields of a picture value returned by @racket[tags-picture]. These @defproc[(id3-picture-bytes [picture id3-picture?]) bytes?]
procedures are useful when the caller wants to process the image bytes directly @defproc[(id3-picture-description [picture id3-picture?]) string?])]{
instead of converting them to a bitmap or writing them to a file. Access the fields of a picture value. These procedures are useful when the
caller wants to process the image bytes directly or pass a picture to another
component.}
@section{Converting to a hash} @section{Converting to a hash}
@defproc[(tags->hash [tags any/c]) hash?]{ @defproc[(tags->hash [tags any/c]) hash?]{
Returns a mutable hash containing the core values copied from the tag handle. Returns a mutable hash containing the core values copied from the tag handle.
The hash contains the keys @racket['valid?], @racket['title], @racket['album], The hash contains the keys @racket['valid?], @racket['read-write?],
@racket['artist], @racket['comment], @racket['composer], @racket['genre], @racket['closed?], @racket['title], @racket['album], @racket['artist],
@racket['year], @racket['track], @racket['length], @racket['sample-rate], @racket['comment], @racket['composer], @racket['genre], @racket['year],
@racket['bit-rate], @racket['channels], @racket['picture], and @racket['keys]. @racket['track], @racket['length], @racket['sample-rate], @racket['bit-rate],
@racket['channels], @racket['picture], and @racket['keys].
The hash is intended as a convenient snapshot for application code. Generic The hash is intended as a convenient snapshot for application code. Generic
property values are not expanded into the hash; use @racket[tags-ref] for those property values are not expanded into the hash; use @racket[tags-ref] for those
values.} values.}
@section{Example} @section{Copying tags and pictures}
The encoder pipeline uses this module for metadata transfer. For FLAC output,
@racket[audio-encode] first writes the audio stream and then opens the resulting
file with @racket[id3-tags] in read-write mode to copy tags and pictures through
TagLib. For Opus output, comments and pictures are supplied to
@tt{libopusenc} before encoding starts, because OpusTags are written at the
start of the Ogg Opus stream.
Applications that need explicit metadata editing should use the read-write API
directly, as in the examples above.
@section[#:tag "taglib-example"]{Example}
@racketblock[ @racketblock[
(define tags (id3-tags "track.flac")) (define tags (id3-tags "track.flac"))
@@ -209,10 +329,10 @@ This chapter documents the public @racketmodname["taglib.rkt"] layer. The
native TagLib calls are delegated to @racketmodname["taglib-ffi.rkt"], but native TagLib calls are delegated to @racketmodname["taglib-ffi.rkt"], but
callers normally should not use that lower level module directly. callers normally should not use that lower level module directly.
The tag handle is implemented as a small Racket object with a private dispatch A read-only tag handle is a Racket-side snapshot. A read-write tag handle keeps
procedure. The native TagLib file is not stored in the handle. This keeps the the native TagLib file open until @racket[tags-close!] is called. Setter
public API simple and prevents native resources from leaking into application procedures update both the native file and the Racket-side cache; the changes
code. are persisted only after @racket[tags-save!] succeeds.
The implementation normalizes generic property names by lower-casing TagLib The implementation normalizes generic property names by lower-casing TagLib
property keys and converting them to symbols. Values remain lists of strings property keys and converting them to symbols. Values remain lists of strings
+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
+144 -104
View File
@@ -3,8 +3,7 @@
(require ffi/unsafe (require ffi/unsafe
ffi/unsafe/define ffi/unsafe/define
"private/utils.rkt" "private/utils.rkt"
"private/downloader.rkt" "private/downloader.rkt")
)
(provide TagLib_File_Type (provide TagLib_File_Type
_TagLib_File-pointer _TagLib_File-pointer
@@ -16,6 +15,7 @@
taglib_file_new_type taglib_file_new_type
taglib_file_is_valid taglib_file_is_valid
taglib_file_free taglib_file_free
taglib_file_save
taglib_file_tag taglib_file_tag
taglib_file_audioproperties taglib_file_audioproperties
@@ -29,6 +29,14 @@
taglib_tag_year taglib_tag_year
taglib_tag_track taglib_tag_track
taglib_tag_set_title
taglib_tag_set_artist
taglib_tag_set_album
taglib_tag_set_comment
taglib_tag_set_genre
taglib_tag_set_year
taglib_tag_set_track
taglib_audioproperties_length taglib_audioproperties_length
taglib_audioproperties_bitrate taglib_audioproperties_bitrate
taglib_audioproperties_samplerate taglib_audioproperties_samplerate
@@ -36,40 +44,23 @@
taglib_property_keys taglib_property_keys
taglib_property_key taglib_property_key
taglib_property_get taglib_property_get
taglib_property_val taglib_property_val
taglib_property_set
taglib_property_set_append
taglib_property_free taglib_property_free
taglib_complex_property_set
taglib_complex_property_set_append
taglib-get-picture taglib-get-picture
) taglib-set-picture
taglib-append-picture
taglib-clear-picture)
(define zlib (get-lib '("zlib" "libz") (linux-lib-versions '("1" #f))))
;(define-runtime-path lib-path ".."); (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 libs (let ((os-type (system-type 'os*)))
; (if (eq? os-type 'windows)
; (list
; (build-path lib-path "lib" "dll" "tag")
; (build-path lib-path "lib" "dll" "tag_c"))
; (let* ((arch (symbol->string (system-type 'arch)))
; (subdir (string-append (symbol->string os-type) "-" arch)))
; (list
; (build-path lib-path "lib" subdir "libtag")
; (build-path lib-path "lib" subdir "libtag_c"))))))
;(define (get-lib l)
; (ffi-lib l '("2" #f)
; #:get-lib-dirs (λ ()
; (cons (build-path ".") (get-lib-search-dirs)))
; #:fail (λ ()
; (error (format "Cannot find library ~a" l)))
; ))
(define zlib (get-lib '("zlib" "libz") '(#f)))
(define libtag (get-lib '("tag" "libtag") '("2" #f)))
(define libtag_c (get-lib '("tag_c" "libtag_c") '("#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)
@@ -97,45 +88,39 @@
dsf dsf
dsdiff dsdiff
shorten shorten
))) matroska)))
(define _TagLib_File-pointer (_cpointer/null 'taglib-file)) (define _TagLib_File-pointer (_cpointer/null 'taglib-file))
(define _TagLib_Tag-pointer (_cpointer/null 'taglib-tag)) (define _TagLib_Tag-pointer (_cpointer/null 'taglib-tag))
(define _TagLib_AudioProperties-pointer (_cpointer/null 'taglib-audioproperties)) (define _TagLib_AudioProperties-pointer (_cpointer/null 'taglib-audioproperties))
; TagLib_File *taglib_file_new(const char *filename);
(define-tag-c-lib taglib_file_new (define-tag-c-lib taglib_file_new
(_fun _string/utf-8 -> _TagLib_File-pointer )) (_fun _string/utf-8 -> _TagLib_File-pointer))
; TAGLIB_C_EXPORT TagLib_File *taglib_file_new_wchar(const wchar_t *filename);
(define-tag-c-lib taglib_file_new_wchar (define-tag-c-lib taglib_file_new_wchar
(_fun _string/utf-16 -> _TagLib_File-pointer )) (_fun _string/utf-16 -> _TagLib_File-pointer))
; TagLib_File *taglib_file_new_type(const char *filename, TagLib_File_Type type);
(define-tag-c-lib taglib_file_new_type (define-tag-c-lib taglib_file_new_type
(_fun _string/utf-8 TagLib_File_Type -> _TagLib_File-pointer)) (_fun _string/utf-8 TagLib_File_Type -> _TagLib_File-pointer))
; TagLib_File *taglib_file_new_type_wchar(const char *filename, TagLib_File_Type type);
(define-tag-c-lib taglib_file_new_type_wchar (define-tag-c-lib taglib_file_new_type_wchar
(_fun _string/utf-16 TagLib_File_Type -> _TagLib_File-pointer)) (_fun _string/utf-16 TagLib_File_Type -> _TagLib_File-pointer))
; void taglib_file_free(TagLib_File *file);
(define-tag-c-lib taglib_file_free (define-tag-c-lib taglib_file_free
(_fun _TagLib_File-pointer -> _void)) (_fun _TagLib_File-pointer -> _void))
; BOOL taglib_file_is_valid(const TagLib_File *file);
(define-tag-c-lib taglib_file_is_valid (define-tag-c-lib taglib_file_is_valid
(_fun _TagLib_File-pointer -> _bool)) (_fun _TagLib_File-pointer -> _bool))
; TagLib_Tag *taglib_file_tag(const TagLib_File *file); (define-tag-c-lib taglib_file_save
(_fun _TagLib_File-pointer -> _bool))
(define-tag-c-lib taglib_file_tag (define-tag-c-lib taglib_file_tag
(_fun _TagLib_File-pointer -> _TagLib_Tag-pointer)) (_fun _TagLib_File-pointer -> _TagLib_Tag-pointer))
; const TagLib_AudioProperties *taglib_file_audioproperties(const TagLib_File *file);
(define-tag-c-lib taglib_file_audioproperties (define-tag-c-lib taglib_file_audioproperties
(_fun _TagLib_File-pointer -> _TagLib_AudioProperties-pointer)) (_fun _TagLib_File-pointer -> _TagLib_AudioProperties-pointer))
; void taglib_tag_free_strings(void);
(define-tag-c-lib taglib_tag_free_strings (define-tag-c-lib taglib_tag_free_strings
(_fun -> _void)) (_fun -> _void))
@@ -150,12 +135,8 @@
(_fun _TagLib_Tag-pointer -> _string/utf-8))) (_fun _TagLib_Tag-pointer -> _string/utf-8)))
((_ name ret-type) ((_ name ret-type)
(define-tag-c-lib name (define-tag-c-lib name
(_fun _TagLib_Tag-pointer -> ret-type))) (_fun _TagLib_Tag-pointer -> ret-type)))))
))
; char *taglib_tag_title(const TagLib_Tag *tag);
; etc..
(tg taglib_tag_title) (tg taglib_tag_title)
(tg taglib_tag_artist) (tg taglib_tag_artist)
(tg taglib_tag_album) (tg taglib_tag_album)
@@ -164,6 +145,23 @@
(tg taglib_tag_year _uint) (tg taglib_tag_year _uint)
(tg taglib_tag_track _uint) (tg taglib_tag_track _uint)
(define-syntax tgs
(syntax-rules ()
((_ name)
(define-tag-c-lib name
(_fun _TagLib_Tag-pointer _string/utf-8 -> _void)))
((_ name arg-type)
(define-tag-c-lib name
(_fun _TagLib_Tag-pointer arg-type -> _void)))))
(tgs taglib_tag_set_title)
(tgs taglib_tag_set_artist)
(tgs taglib_tag_set_album)
(tgs taglib_tag_set_comment)
(tgs taglib_tag_set_genre)
(tgs taglib_tag_set_year _uint)
(tgs taglib_tag_set_track _uint)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; audio properties ;; audio properties
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -172,11 +170,7 @@
(syntax-rules () (syntax-rules ()
((_ name) ((_ name)
(define-tag-c-lib name (define-tag-c-lib name
(_fun _TagLib_AudioProperties-pointer -> _int))) (_fun _TagLib_AudioProperties-pointer -> _int)))))
))
; int taglib_audioproperties_length(const TagLib_AudioProperties *audioProperties);
; etc...
(ap taglib_audioproperties_length) (ap taglib_audioproperties_length)
(ap taglib_audioproperties_bitrate) (ap taglib_audioproperties_bitrate)
@@ -184,24 +178,29 @@
(ap taglib_audioproperties_channels) (ap taglib_audioproperties_channels)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; keys in the propertymap ;; property map
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; char** taglib_property_keys(const TagLib_File *file);
(define-tag-c-lib taglib_property_keys (define-tag-c-lib taglib_property_keys
(_fun _TagLib_File-pointer -> (_ptr i _string/utf-8))) (_fun _TagLib_File-pointer -> (_ptr i _string/utf-8)))
(define (taglib_property_key keys i) (define (taglib_property_key keys i)
(ptr-ref keys _string/utf-8 i)) (ptr-ref keys _string/utf-8 i))
;char** taglib_property_get(const TagLib_File *file, const char *prop);
(define-tag-c-lib taglib_property_get (define-tag-c-lib taglib_property_get
(_fun _TagLib_File-pointer _string/utf-8 -> (_ptr i _string/utf-8))) (_fun _TagLib_File-pointer _string/utf-8 -> (_ptr i _string/utf-8)))
(define (taglib_property_val prop i) (define (taglib_property_val prop i)
(ptr-ref prop _string/utf-8 i)) (ptr-ref prop _string/utf-8 i))
; void taglib_property_free(char **props); ;; value may be NULL to clear the property.
(define-tag-c-lib taglib_property_set
(_fun _TagLib_File-pointer _string/utf-8 _pointer -> _void))
;; value may be NULL to clear all values for the property.
(define-tag-c-lib taglib_property_set_append
(_fun _TagLib_File-pointer _string/utf-8 _pointer -> _void))
(define-tag-c-lib taglib_property_free (define-tag-c-lib taglib_property_free
(_fun _pointer -> _void)) (_fun _pointer -> _void))
@@ -209,40 +208,12 @@
;; Picture data ;; Picture data
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;typedef struct {
; char *mimeType;
; char *description;
; char *pictureType;
; char *data;
; unsigned int size;
;} TagLib_Complex_Property_Picture_Data;
(define-cstruct _TagLib_Complex_Property_Picture_Data (define-cstruct _TagLib_Complex_Property_Picture_Data
( ([mimeType _string/utf-8]
[mimeType _string/utf-8]
[description _string/utf-8] [description _string/utf-8]
[pictureType _string/utf-8] [pictureType _string/utf-8]
[data _pointer] [data _pointer]
[size _uint] [size _uint]))
))
; TagLib_Complex_Property_Attribute*** properties = * taglib_complex_property_get(file, "PICTURE");
; * TagLib_File *file = taglib_file_new("myfile.mp3");
; * TagLib_Complex_Property_Attribute*** properties =
; * taglib_complex_property_get(file, "PICTURE");
; * TagLib_Complex_Property_Picture_Data picture;
; * taglib_picture_from_complex_property(properties, &picture);
; * // Do something with picture.mimeType, picture.description,
; * // picture.pictureType, picture.data, picture.size, e.g. extract it.
; * FILE *fh = fopen("mypicture.jpg", "wb");
; * if(fh) {
; * fwrite(picture.data, picture.size, 1, fh);
; * fclose(fh);
; * }
; * taglib_complex_property_free(properties);
(define _Complex_Property_Attribute-pointer (_cpointer/null 'taglib-complex-property-attribute)) (define _Complex_Property_Attribute-pointer (_cpointer/null 'taglib-complex-property-attribute))
@@ -257,38 +228,107 @@
(define-tag-c-lib taglib_complex_property_free (define-tag-c-lib taglib_complex_property_free
(_fun _Complex_Property_Attribute-pointer -> _void)) (_fun _Complex_Property_Attribute-pointer -> _void))
;TAGLIB_C_EXPORT char** taglib_complex_property_keys(const TagLib_File *file);
(define-tag-c-lib taglib_complex_property_keys (define-tag-c-lib taglib_complex_property_keys
(_fun _TagLib_File-pointer -> (_ptr i _string/utf-8))) (_fun _TagLib_File-pointer -> (_ptr i _string/utf-8)))
; void taglib_complex_property_free_keys(char **keys);
(define-tag-c-lib taglib_complex_property_free_keys (define-tag-c-lib taglib_complex_property_free_keys
(_fun _pointer -> _void)) (_fun _pointer -> _void))
(define (taglib-get-picture tag-file) ;; TagLib_Variant is { enum type; unsigned int size; union value; }.
(define (cp s) (string-append s "")) ;; For writing pictures we only use pointer-valued union members: stringValue
(define (to-bytestring data size) ;; and byteVectorValue. A pointer-sized field has the same size/alignment as
;; the union on the supported ABIs.
(define TagLib_Variant_ByteVector 9)
(define TagLib_Variant_String 7)
(let* ((v (make-vector size 0)) (define-cstruct _TagLib_Variant
(i 0)) ([type _int]
(while (< i size) [size _uint]
(vector-set! v (ptr-ref data _byte i) i) [value _pointer]))
(set! i (+ i 1)))
v)) (define-cstruct _TagLib_Complex_Property_Attribute
([key _pointer]
[value _TagLib_Variant]))
(define-tag-c-lib taglib_complex_property_set
(_fun _TagLib_File-pointer _string/utf-8 _pointer -> _bool))
(define-tag-c-lib taglib_complex_property_set_append
(_fun _TagLib_File-pointer _string/utf-8 _pointer -> _bool))
(define (bytes->malloc-ptr bs [nul? #f])
(define len (bytes-length bs))
(define ptr (malloc _byte (+ len (if nul? 1 0)) 'atomic-interior))
(for ([i (in-range len)]) (ptr-set! ptr _byte i (bytes-ref bs i)))
(when nul? (ptr-set! ptr _byte len 0))
ptr)
(define (string/bytes->malloc-cstring who v)
(define bs
(cond
[(bytes? v) v]
[(string? v) (string->bytes/utf-8 v)]
[(symbol? v) (string->bytes/utf-8 (symbol->string v))]
[(number? v) (string->bytes/utf-8 (number->string v))]
[else (raise-argument-error who "(or/c string? bytes? symbol? number?)" v)]))
(bytes->malloc-ptr bs #t))
(define (string->malloc-cstring s)
(string/bytes->malloc-cstring 'string->malloc-cstring s))
(define (picture->complex-property data size description mimetype picture-type)
(define data-ptr (bytes->malloc-ptr data #f))
(define data-key (string->malloc-cstring "data"))
(define mime-key (string->malloc-cstring "mimeType"))
(define desc-key (string->malloc-cstring "description"))
(define type-key (string->malloc-cstring "pictureType"))
(define mime-ptr (string->malloc-cstring mimetype))
(define desc-ptr (string->malloc-cstring description))
(define type-ptr (string->malloc-cstring picture-type))
(define data-attr (make-TagLib_Complex_Property_Attribute data-key (make-TagLib_Variant TagLib_Variant_ByteVector size data-ptr)))
(define mime-attr (make-TagLib_Complex_Property_Attribute mime-key (make-TagLib_Variant TagLib_Variant_String 0 mime-ptr)))
(define desc-attr (make-TagLib_Complex_Property_Attribute desc-key (make-TagLib_Variant TagLib_Variant_String 0 desc-ptr)))
(define type-attr (make-TagLib_Complex_Property_Attribute type-key (make-TagLib_Variant TagLib_Variant_String 0 type-ptr)))
(define propv (malloc _pointer 5 'atomic-interior))
(ptr-set! propv _pointer 0 data-attr)
(ptr-set! propv _pointer 1 mime-attr)
(ptr-set! propv _pointer 2 desc-attr)
(ptr-set! propv _pointer 3 type-attr)
(ptr-set! propv _pointer 4 #f)
;; Return keepalive values as well as the pointer array. TagLib copies during
;; taglib_complex_property_set(), but all buffers must remain live for the call.
(values propv (list data-ptr data-key mime-key desc-key type-key mime-ptr desc-ptr type-ptr
data-attr mime-attr desc-attr type-attr propv)))
(define (taglib-set-picture tag-file mimetype picture-type description data)
(define-values (props keepalive)
(picture->complex-property data (bytes-length data) description mimetype picture-type))
(define ok? (taglib_complex_property_set tag-file "PICTURE" props))
keepalive
ok?)
(define (taglib-append-picture tag-file mimetype picture-type description data)
(define-values (props keepalive)
(picture->complex-property data (bytes-length data) description mimetype picture-type))
(define ok? (taglib_complex_property_set_append tag-file "PICTURE" props))
keepalive
ok?)
(define (taglib-clear-picture tag-file)
(taglib_complex_property_set tag-file "PICTURE" #f))
(define (taglib-get-picture tag-file)
(define (cp s) (if (eq? s #f) "" (string-append s "")))
(let ((props (taglib_complex_property_get tag-file "PICTURE"))) (let ((props (taglib_complex_property_get tag-file "PICTURE")))
(if (eq? props #f) (if (eq? props #f)
#f #f
(let ((pd (make-TagLib_Complex_Property_Picture_Data #f #f #f #f 0))) (let ((pd (make-TagLib_Complex_Property_Picture_Data #f #f #f #f 0)))
(taglib_picture_from_complex_property props pd) (taglib_picture_from_complex_property props pd)
(let* ((mimetype (cp (TagLib_Complex_Property_Picture_Data-mimeType pd))) (let* ((mimetype (cp (TagLib_Complex_Property_Picture_Data-mimeType pd)))
(description (cp (TagLib_Complex_Property_Picture_Data-description pd))) (description (cp (TagLib_Complex_Property_Picture_Data-description pd)))
(type (cp (TagLib_Complex_Property_Picture_Data-pictureType pd))) (type (cp (TagLib_Complex_Property_Picture_Data-pictureType pd)))
(size (TagLib_Complex_Property_Picture_Data-size pd)) (size (TagLib_Complex_Property_Picture_Data-size pd))
(data (cast (TagLib_Complex_Property_Picture_Data-data pd) (data (cast (TagLib_Complex_Property_Picture_Data-data pd) _pointer (_bytes o size))))
_pointer
(_bytes o size)))
)
(let ((r (list mimetype description type size data))) (let ((r (list mimetype description type size data)))
(taglib_complex_property_free props) (taglib_complex_property_free props)
r)))) r))))))
))
+278
View File
@@ -0,0 +1,278 @@
#lang racket/base
(require rackunit
racket/class
racket/draw
racket/file
racket/list
racket/path
racket/runtime-path
"taglib.rkt")
(provide run-taglib-tests
run-taglib-tests/verbose
current-taglib-test-verbosity
test-audio-dir
taglib-read-files
taglib-write-files)
;; These tests expect the repository hans/racket-audio-test next to this
;; package checkout, matching the layout already used by tests.rkt:
;;
;; parent/
;; racket-audio/
;; racket-audio-test/
;;
;; The tests are defensive: missing test files are skipped, but existing files
;; are tested. Write tests always work on a temporary copy and never modify the
;; original test audio files.
(define current-taglib-test-verbosity (make-parameter 'normal))
(define (taglib-test-verbose?)
(memq (current-taglib-test-verbosity) '(verbose very-verbose)))
(define (taglib-test-note fmt . args)
(when (taglib-test-verbose?)
(apply printf fmt args)
(newline)
(flush-output)))
(define-syntax-rule (taglib-test-case name body ...)
(test-case name
(taglib-test-note "[taglib] running: ~a" name)
body ...
(taglib-test-note "[taglib] ok: ~a" name)))
(define-runtime-path test-audio-dir "../racket-audio-test")
(define taglib-read-files
'("idyll.flac"
"idyll.m4a"
"idyll.mp3"
"idyll.ogg"
"idyll.opus"
"mahler-1.mp3"
"mahler-1.ogg"
"mahler-1.opus"
"mahler-2.mp3"
"mahler-2.ogg"
"mahler-2.opus"
"ff-16b-2c-44100hz.flac"
"ff-16b-2c-44100hz.m4a"
"ff-16b-2c-44100hz.mp3"
"ff-16b-2c-44100hz.ogg"
"ff-16b-2c-44100hz.opus"))
;; Keep the write matrix deliberately small. These formats should cover the
;; main TagLib backends used by the package without making the test suite slow.
(define taglib-write-files
'("idyll.flac"
"idyll.mp3"
"idyll.m4a"
"idyll.ogg"
"idyll.opus"))
(define (existing-test-files names)
(for/list ([name (in-list names)]
#:when (file-exists? (build-path test-audio-dir name)))
(build-path test-audio-dir name)))
(define (taglib-usable?)
(with-handlers ([exn:fail? (lambda (_) #f)])
(define files (existing-test-files taglib-read-files))
(and (pair? files)
(let ([tags (id3-tags (car files))])
(and (tags-valid? tags) #t)))))
(define (copy-test-file-to-temp src)
(define dst (make-temporary-file (format "racket-audio-taglib-~a-~~a~a"
(path->string (file-name-from-path src))
(or (path-get-extension src) #""))))
(copy-file src dst #t)
dst)
(define (check-nonnegative/name name v)
(check-true (and (exact-integer? v) (>= v -1)) name))
(define (check-readable-snapshot path)
(taglib-test-case (format "read-only snapshot: ~a" (file-name-from-path path))
(define tags (id3-tags path))
(check-true (tags-valid? tags))
(check-false (tags-read-write? tags))
(check-true (tags-closed? tags))
(check-pred string? (tags-title tags))
(check-pred string? (tags-album tags))
(check-pred string? (tags-artist tags))
(check-pred string? (tags-comment tags))
(check-pred string? (tags-genre tags))
(check-nonnegative/name "year" (tags-year tags))
(check-nonnegative/name "track" (tags-track tags))
(check-nonnegative/name "length" (tags-length tags))
(check-nonnegative/name "sample-rate" (tags-sample-rate tags))
(check-nonnegative/name "bit-rate" (tags-bit-rate tags))
(check-nonnegative/name "channels" (tags-channels tags))
(check-true (list? (tags-keys tags)))
;; A read-only snapshot must still be usable after the native TagLib file
;; has been closed. This protects the audio playback path from stale file
;; handles/locks after metadata reading.
(check-pred hash? (tags->hash tags))
(check-exn exn:fail? (lambda () (tags-title! tags "must fail")))))
(define (check-call-with-closes path)
(taglib-test-case (format "call-with-id3-tags closes read-write handle: ~a" (file-name-from-path path))
(define captured #f)
(with-handlers ([exn:fail? void])
(call-with-id3-tags path #:mode 'read-write
(lambda (tags)
(set! captured tags)
(check-true (tags-read-write? tags))
(check-false (tags-closed? tags))
(error 'expected-test-exception "force close path"))))
(check-true (tags-closed? captured))))
(define (check-simple-write-roundtrip path)
(taglib-test-case (format "tag write/read/clear roundtrip: ~a" (file-name-from-path path))
(define tmp (copy-test-file-to-temp path))
(displayln (format "tmp = ~a" tmp))
(dynamic-wind
void
(lambda ()
(define title (format "Racket Audio TagLib Test ~a" (current-inexact-milliseconds)))
(call-with-id3-tags tmp #:mode 'read-write
(lambda (tags)
(check-true (tags-valid? tags))
(check-true (tags-read-write? tags))
(check-false (tags-closed? tags))
(tags-title! tags title)
(tags-album! tags "Racket Audio Test Album")
(tags-artist! tags "Racket Audio Test Artist")
(tags-comment! tags "Written by racket-audio taglib-tests.rkt")
(tags-genre! tags "Test")
(tags-year! tags 2026)
(tags-track! tags 7)
(tags-composer! tags "Racket Composer")
(tags-album-artist! tags "Racket Album Artist")
(tags-disc-number! tags 2)
(tags-set-values! tags 'performer '("Performer One" "Performer Two"))
(check-true (tags-save! tags))))
(define reread (id3-tags tmp))
(check-true (tags-valid? reread))
(check-true (tags-closed? reread))
(check-equal? (tags-title reread) title)
(check-equal? (tags-album reread) "Racket Audio Test Album")
(check-equal? (tags-artist reread) "Racket Audio Test Artist")
(check-equal? (tags-comment reread) "Written by racket-audio taglib-tests.rkt")
(check-equal? (tags-genre reread) "Test")
(check-equal? (tags-year reread) 2026)
(check-equal? (tags-track reread) 7)
(check-equal? (tags-composer reread) "Racket Composer")
(check-equal? (tags-album-artist reread) "Racket Album Artist")
(check-equal? (tags-disc-number reread) 2)
(check-equal? (tags-ref reread 'performer) '("Performer One" "Performer Two"))
(call-with-id3-tags tmp #:mode 'read-write
(lambda (tags)
(tags-title! tags 'clear)
(tags-year! tags 'clear)
(tags-track! tags 'clear)
(tags-clear! tags 'composer)
(tags-clear! tags 'performer)
(check-true (tags-save! tags))))
(define cleared (id3-tags tmp))
(check-equal? (tags-title cleared) "")
(check-equal? (tags-year cleared) -1)
(check-equal? (tags-track cleared) -1)
(check-equal? (tags-composer cleared) "")
(check-false (tags-ref cleared 'performer)))
(lambda ()
(when (file-exists? tmp) (delete-file tmp))))))
(define (make-test-bitmap)
(define bm (make-object bitmap% 4 4))
(define dc (new bitmap-dc% [bitmap bm]))
(send dc set-pen "black" 1 'solid)
(send dc set-brush "white" 'solid)
(send dc draw-rectangle 0 0 4 4)
(send dc set-pen "black" 1 'solid)
(send dc draw-line 0 0 3 3)
(send dc set-bitmap #f)
bm)
(define (check-picture-roundtrip path)
(taglib-test-case (format "picture write/read/clear roundtrip: ~a" (file-name-from-path path))
(define tmp (copy-test-file-to-temp path))
(dynamic-wind
void
(lambda ()
(define picture (make-tags-picture-from-bitmap (make-test-bitmap)
"Front Cover"
#:mimetype "image/png"
#:description "Racket test cover"))
(call-with-id3-tags tmp #:mode 'read-write
(lambda (tags)
(tags-picture! tags picture)
(check-true (tags-save! tags))))
(define reread (id3-tags tmp))
(define p (tags-picture reread))
(check-true (id3-picture? p))
(check-equal? (id3-picture-mimetype p) "image/png")
(check-equal? (id3-picture-kind p) "Front Cover")
(check-equal? (id3-picture-description p) "Racket test cover")
(check-true (> (id3-picture-size p) 0))
(check-true (is-a? (tags-picture->bitmap reread) bitmap%))
(call-with-id3-tags tmp #:mode 'read-write
(lambda (tags)
(tags-clear-picture! tags)
(check-true (tags-save! tags))))
(check-false (tags-picture (id3-tags tmp)))
)
(lambda ()
(when (file-exists? tmp) (delete-file tmp))))))
(define (run-taglib-tests [verbosity 'normal])
(unless (memq verbosity '(quiet normal verbose very-verbose))
(raise-argument-error 'run-taglib-tests "(or/c 'quiet 'normal 'verbose 'very-verbose)" verbosity))
(parameterize ([current-taglib-test-verbosity verbosity])
(cond
[(not (directory-exists? test-audio-dir))
(unless (eq? verbosity 'quiet)
(printf "Skipping TagLib tests: test audio directory not found: ~a\n" test-audio-dir))
(void)]
[(not (taglib-usable?))
(unless (eq? verbosity 'quiet)
(printf "Skipping TagLib tests: TagLib runtime is not available or no readable test file was found.\n"))
(void)]
[else
(define read-files (existing-test-files taglib-read-files))
(define write-files (existing-test-files taglib-write-files))
(taglib-test-note "[taglib] test audio directory: ~a" test-audio-dir)
(taglib-test-note "[taglib] read files: ~a" (length read-files))
(taglib-test-note "[taglib] write files: ~a" (length write-files))
(for ([path (in-list read-files)]) (check-readable-snapshot path))
(when (pair? write-files)
;; call-with close behavior only needs one writable copy.
(define tmp (copy-test-file-to-temp (car write-files)))
(dynamic-wind void
(lambda () (check-call-with-closes tmp))
(lambda () (when (file-exists? tmp) (delete-file tmp)))))
(for ([path (in-list write-files)]) (check-simple-write-roundtrip path))
;; Exercise picture writing on FLAC first, because it is the least
;; ambiguous container for embedded cover-art roundtrips with TagLib.
(define flac (build-path test-audio-dir "idyll.flac"))
(when (file-exists? flac) (check-picture-roundtrip flac))
(taglib-test-note "[taglib] done")])))
(define (run-taglib-tests/verbose)
(run-taglib-tests 'verbose))
(module+ test
(run-taglib-tests))
(module+ main
(run-taglib-tests))
+520 -182
View File
@@ -2,12 +2,19 @@
(require "taglib-ffi.rkt" (require "taglib-ffi.rkt"
"private/utils.rkt" "private/utils.rkt"
racket/string ffi/unsafe
racket/draw) racket/class
racket/draw
racket/string)
(provide id3-tags (provide id3-tags
call-with-id3-tags
tags-valid? tags-valid?
tags-read-write?
tags-closed?
tags-close!
tags-save!
tags-title tags-title
tags-album tags-album
@@ -20,6 +27,17 @@
tags-disc-number tags-disc-number
tags-album-artist tags-album-artist
tags-title!
tags-album!
tags-artist!
tags-comment!
tags-year!
tags-genre!
tags-track!
tags-composer!
tags-disc-number!
tags-album-artist!
tags-length tags-length
tags-sample-rate tags-sample-rate
tags-bit-rate tags-bit-rate
@@ -27,202 +45,514 @@
tags-keys tags-keys
tags-ref tags-ref
tags-set!
tags-set-values!
tags-append!
tags-clear!
tags-picture tags-picture
tags-picture!
tags-append-picture!
tags-clear-picture!
tags-picture->bitmap tags-picture->bitmap
tags-picture->file tags-picture->file
tags-picture->kind tags-picture->kind
tags-picture->mimetype tags-picture->mimetype
tags-picture->description
tags-picture->size tags-picture->size
tags-picture->ext tags-picture->ext
tags->hash tags->hash
make-tags-picture
make-tags-picture-from-bitmap
id3-picture?
id3-picture-mimetype id3-picture-mimetype
id3-picture-kind id3-picture-kind
id3-picture-size id3-picture-size
id3-picture-bytes id3-picture-bytes
id3-picture-description
PIC-KIND-OTHER
PIC-KIND-PNG-ICON32x32
PIC-KIND-OTHER-ICON
PIC-KIND-COVER
PIC-KIND-COVER-FRONT
PIC-KIND-COVER-BACK
PIC-KIND-LEAFLET
PIC-KIND-MEDIA
PIC-KIND-LEAD-SOLOIST
PIC-KIND-ARTIST
PIC-KIND-CONDUCTOR
PIC-KIND-BAND
PIC-KIND-ORCHESTRA
PIC-KIND-COMPOSER
PIC-KIND-LYRICIST
PIC-KIND-SONG-WRITER
PIC-KIND-TEXT-WRITER
PIC-KIND-RECORDING-LOCATION
PIC-KIND-DURING-RECORDING
PIC-KIND-DURING-PERFORMANCE
PIC-KIND-SCREEN-CAPTURE
PIC-KIND-BRIGHT-COLORED-FISH
PIC-KIND-ILLUSTRATION
PIC-KIND-BAND-LOGO
PIC-KIND-PUBLISHER-LOGO
) )
(define-struct id3-tag-struct ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(handle)) ;; Picture kinds
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;$00: Other
;$01: 32x32 pixels 'file icon' (PNG only)
;$02: Other file icon
;$03: Cover (front) (The most common standard for album art)
;$04: Cover (back)
;$05: Leaflet page
;$06: Media (e.g., label side of CD)
;$07: Lead artist/soloist
;$08: Artist/performer
;$09: Conductor
;$0A: Band/Orchestra
;$0B: Composer
;$0C: Lyricist/text writer
;$0D: Recording location
;$0E: During recording
;$0F: During performance
;$10: Movie/video screen capture
;$11: A bright colored fish
;$12: Illustration
;$13: Band/artist logotype
;$14: Publisher/Studio logotype
(define-struct id3-picture (define PIC-KIND-OTHER 0)
(mimetype kind size bytes)) (define PIC-KIND-PNG-ICON32x32 1)
(define PIC-KIND-OTHER-ICON 2)
(define PIC-KIND-COVER 3)
(define PIC-KIND-COVER-FRONT 3)
(define PIC-KIND-COVER-BACK 4)
(define PIC-KIND-LEAFLET 5)
(define PIC-KIND-MEDIA 6)
(define PIC-KIND-LEAD-SOLOIST 7)
(define PIC-KIND-ARTIST 8)
(define PIC-KIND-CONDUCTOR 9)
(define PIC-KIND-BAND 10)
(define PIC-KIND-ORCHESTRA 10)
(define PIC-KIND-COMPOSER 11)
(define PIC-KIND-LYRICIST 12)
(define PIC-KIND-SONG-WRITER 12)
(define PIC-KIND-TEXT-WRITER 12)
(define PIC-KIND-RECORDING-LOCATION 13)
(define PIC-KIND-DURING-RECORDING 14)
(define PIC-KIND-DURING-PERFORMANCE 15)
(define PIC-KIND-SCREEN-CAPTURE 16)
(define PIC-KIND-BRIGHT-COLORED-FISH 17)
(define PIC-KIND-ILLUSTRATION 18)
(define PIC-KIND-BAND-LOGO 19)
(define PIC-KIND-PUBLISHER-LOGO 20)
(define (id3-tags file*)
(let ((file (if (path? file*) (path->string file*) file*)) (define-struct id3-tag-struct (handle))
(valid? #f) (define-struct id3-picture (mimetype kind size bytes description))
(title "")
(album "") (define clear-tag-value 'clear)
(artist "")
(comment "") (define (normal-mode mode)
(year -1) (cond
(genre "") [(or (eq? mode 'read) (eq? mode 'read-only)) 'read]
(track -1) [(or (eq? mode 'write) (eq? mode 'read-write)) 'read-write]
(length -1) [else (raise-argument-error 'id3-tags "(or/c 'read 'read-only 'read-write 'write)" mode)]))
(sample-rate -1)
(bit-rate -1) (define (file->string file*)
(channels -1) (if (path? file*) (path->string file*) file*))
(key-store (make-hash))
(composer "") (define (copy-string s)
(album-artist "") (if (eq? s #f) "" (string-append s "")))
(disc-number -1)
(picture #f)) (define (property-name k)
(let ((tag-file (taglib_file_new file))) (cond
(if (eq? tag-file #f) [(symbol? k) (string-upcase (symbol->string k))]
[(string? k) k]
[else (raise-argument-error 'tag-property "(or/c symbol? string?)" k)]))
(define (property-symbol k)
(string->symbol (string-downcase (property-name k))))
(define (first-property h key [default ""])
(let ((v (hash-ref h key #f)))
(cond
[(and (pair? v) (string? (car v))) (car v)]
[(string? v) v]
[else default])))
(define (first-property-number h key [default -1])
(let ((n (string->number (first-property h key (number->string default)))))
(if n n default)))
(define (string-list? v)
(and (list? v) (andmap string? v)))
(define (bitmap->encoded-bytes bm mimetype)
(define kind
(cond
[(or (string-ci=? mimetype "image/jpeg") (string-ci=? mimetype "image/jpg")) 'jpeg]
[(string-ci=? mimetype "image/png") 'png]
[else (error 'make-tags-picture-from-bitmap
"unsupported bitmap mimetype: ~a; use image/png or image/jpeg" mimetype)]))
(define out (open-output-bytes))
(unless (send bm save-file out kind)
(error 'make-tags-picture-from-bitmap "could not encode bitmap as ~a" mimetype))
(get-output-bytes out))
(define picture-type-names
(hash 0 "Other"
1 "File Icon"
2 "Other File Icon"
3 "Front Cover"
4 "Back Cover"
5 "Leaflet Page"
6 "Media"
7 "Lead Artist"
8 "Artist"
9 "Conductor"
10 "Band"
11 "Composer"
12 "Lyricist"
13 "Recording Location"
14 "During Recording"
15 "During Performance"
16 "Movie Screen Capture"
17 "Coloured Fish"
18 "Illustration"
19 "Band Logo"
20 "Publisher Logo"))
(define (picture-kind->string kind)
(cond
[(string? kind) kind]
[(symbol? kind) (string-titlecase (regexp-replace* #rx"-" (symbol->string kind) " "))]
[(integer? kind) (hash-ref picture-type-names kind (number->string kind))]
[else (raise-argument-error 'make-tags-picture "(or/c string? symbol? integer?)" kind)]))
(define (make-tags-picture mimetype kind data #:description [description ""])
(define bytes
(cond
[(bytes? data) data]
[(is-a? data bitmap%) (bitmap->encoded-bytes data mimetype)]
[else (raise-argument-error 'make-tags-picture "(or/c bytes? (is-a?/c bitmap%))" data)]))
(make-id3-picture mimetype (picture-kind->string kind) (bytes-length bytes) bytes description))
(define (make-tags-picture-from-bitmap bm kind #:mimetype [mimetype "image/png"] #:description [description ""])
(make-tags-picture mimetype kind bm #:description description))
(define (open-tag-file file)
(let ((tag-file (if (eq? (system-type 'os) 'windows)
(taglib_file_new_wchar file)
(taglib_file_new file))))
(cond
((eq? tag-file #f) #f)
((taglib_file_is_valid tag-file) tag-file)
(else
(taglib_file_free tag-file)
#f))
)
)
(define (read-property-map tag-file)
(define key-store (make-hash))
(let* ((keys (taglib_property_keys tag-file))
(i 0)
(key (and keys (taglib_property_key keys i)))
(key-list '()))
(while (not (eq? key #f))
(set! key-list (append key-list (list (copy-string key))))
(set! i (+ i 1))
(set! key (taglib_property_key keys i)))
(for-each
(lambda (key)
(let ((props (taglib_property_get tag-file key)))
(let* ((vals '())
(i 0)
(val (and props (taglib_property_val props i))))
(while (not (eq? val #f))
(set! vals (append vals (list (copy-string val))))
(set! i (+ i 1))
(set! val (taglib_property_val props i)))
(when props (taglib_property_free props))
(hash-set! key-store (string->symbol (string-downcase key)) vals))))
key-list))
key-store)
(define (read-picture tag-file)
(let ((p (taglib-get-picture tag-file)))
(if (eq? p #f)
#f
(let ((mimetype (car p))
(description (cadr p))
(kind (caddr p))
(size (cadddr p))
(bytes (car (cddddr p))))
(make-id3-picture mimetype kind size bytes description)))))
(define (id3-tags file* #:mode [mode 'read])
(define file (file->string file*))
(define actual-mode (normal-mode mode))
(define read-write? (eq? actual-mode 'read-write))
(define valid? #f)
(define closed? #t)
(define tag-file #f)
(define tag #f)
(define title "")
(define album "")
(define artist "")
(define comment "")
(define year -1)
(define genre "")
(define track -1)
(define length -1)
(define sample-rate -1)
(define bit-rate -1)
(define channels -1)
(define key-store (make-hash))
(define composer "")
(define album-artist "")
(define disc-number -1)
(define picture #f)
(define (refresh-derived!)
(set! composer (first-property key-store 'composer ""))
(set! album-artist (first-property key-store 'albumartist ""))
(set! disc-number (first-property-number key-store 'discnumber -1)))
(define (open-and-read!)
(set! tag-file (open-tag-file file))
(if (eq? tag-file #f)
(begin
(set! valid? #f) (set! valid? #f)
(set! valid? (taglib_file_is_valid tag-file))) (warn-sound "Could not open file ~a" file))
(begin
(set! valid? #t)
(set! closed? #f)
(set! tag (taglib_file_tag tag-file))
(let ((ap (taglib_file_audioproperties tag-file)))
(set! title (copy-string (taglib_tag_title tag)))
(set! album (copy-string (taglib_tag_album tag)))
(set! artist (copy-string (taglib_tag_artist tag)))
(set! comment (copy-string (taglib_tag_comment tag)))
(set! genre (copy-string (taglib_tag_genre tag)))
(set! year (let ((v (taglib_tag_year tag))) (if (zero? v) -1 v)))
(set! track (let ((v (taglib_tag_track tag))) (if (zero? v) -1 v)))
(set! length (taglib_audioproperties_length ap))
(set! sample-rate (taglib_audioproperties_samplerate ap))
(set! bit-rate (taglib_audioproperties_bitrate ap))
(set! channels (taglib_audioproperties_channels ap))
(set! key-store (read-property-map tag-file))
(refresh-derived!)
(set! picture (read-picture tag-file))
(taglib_tag_free_strings)
(unless read-write? (close!))))))
(unless valid? (define (close!)
(when (eq? (system-type 'os) 'windows) (unless closed?
(dbg-sound "Could not open file ~a, trying wchar version on windows" file) (taglib_file_free tag-file)
(unless (eq? tag-file #f) (set! tag-file #f)
(taglib_file_free tag-file)) (set! tag #f)
(set! tag-file (taglib_file_new_wchar file)) (set! closed? #t))
(if (eq? tag-file #f) (void))
(set! valid? #f)
(set! valid? (taglib_file_is_valid tag-file)))))
(unless valid? (define (ensure-open! who)
(warn-sound "Could not open file ~a" file) (unless valid? (error who "tag handle is invalid: ~a" file))
(unless (eq? tag-file #f) (unless read-write?
(taglib_file_free tag-file) (error who "tag handle is read-only for ~a; open with #:mode 'read-write" file))
(set! tag-file #f))) (when closed? (error who "tag handle is closed: ~a" file)))
(when valid? (define (set-property-cache! key vals)
(let ((tag (taglib_file_tag tag-file)) (define sym (property-symbol key))
(ap (taglib_file_audioproperties tag-file)) (if (null? vals) (hash-remove! key-store sym) (hash-set! key-store sym vals))
(cp (lambda (s) (string-append s ""))) (refresh-derived!))
)
(set! title (cp (taglib_tag_title tag)))
(set! album (cp (taglib_tag_album tag)))
(set! artist (cp (taglib_tag_artist tag)))
(set! comment (cp (taglib_tag_comment tag)))
(set! genre (cp (taglib_tag_genre tag)))
(set! year (taglib_tag_year tag))
(set! track (taglib_tag_track tag))
(set! length (taglib_audioproperties_length ap)) (define (string->cptr s)
(set! sample-rate (taglib_audioproperties_samplerate ap)) (define bs (string->bytes/utf-8 s))
(set! bit-rate (taglib_audioproperties_bitrate ap)) (define len (bytes-length bs))
(set! channels (taglib_audioproperties_channels ap)) (define ptr (malloc _byte (+ len 1) 'atomic-interior))
(for ([i (in-range len)]) (ptr-set! ptr _byte i (bytes-ref bs i)))
(ptr-set! ptr _byte len 0)
ptr)
(let* ((keys (taglib_property_keys tag-file)) (define (apply-string! who value setter cache!)
(i 0) (ensure-open! who)
(key (taglib_property_key keys i)) (cond
(key-list '()) [(eq? value clear-tag-value) (setter tag "") (cache! "")]
) [(string? value) (setter tag value) (cache! value)]
(while (not (eq? key #f)) [else (raise-argument-error who "(or/c string? 'clear)" value)]))
(set! key-list (append key-list (list (cp key))))
(set! i (+ i 1))
(set! key (taglib_property_key keys i)))
(for-each (lambda (key)
(let ((props (taglib_property_get tag-file key)))
(let* ((vals '())
(i 0)
(val (taglib_property_val props i)))
(while (not (eq? val #f))
(set! vals (append vals (list (cp val))))
(set! i (+ i 1))
(set! val (taglib_property_val props i)))
(taglib_property_free props)
(hash-set! key-store
(string->symbol
(string-downcase key)) vals)
)))
key-list)
(set! composer (hash-ref key-store 'composer ""))
(set! album-artist (hash-ref key-store 'albumartist ""))
(set! disc-number (string->number
(car
(hash-ref key-store 'discnumber (list "-1")))))
)
; picture (define (apply-uint! who value setter cache!)
(let ((p (taglib-get-picture tag-file))) (ensure-open! who)
(if (eq? p #f) (cond
(set! picture #f) [(eq? value clear-tag-value) (setter tag 0) (cache! -1)]
(let ((mimetype (car p)) [(and (exact-nonnegative-integer? value) (<= value #xffffffff))
(kind (caddr p)) (setter tag value) (cache! value)]
(size (cadddr p)) [else (raise-argument-error who "(or/c exact-nonnegative-integer? 'clear)" value)]))
(bytes (car (cddddr p))))
(set! picture (make-id3-picture mimetype kind size bytes))
)))
; cleaning up (define (set-one-property! who key value #:append? [append? #f])
(taglib_tag_free_strings) (ensure-open! who)
(taglib_file_free tag-file) (cond
) [(eq? value clear-tag-value)
) (if append?
(let ((handle (taglib_property_set_append tag-file (property-name key) #f)
(lambda (v . args) (taglib_property_set tag-file (property-name key) #f))
(cond (set-property-cache! key '())]
[(eq? v 'valid?) valid?] [(string? value)
[(eq? v 'title) title] (if append?
[(eq? v 'album) album] (taglib_property_set_append tag-file (property-name key) (string->cptr value))
[(eq? v 'artist) artist] (taglib_property_set tag-file (property-name key) (string->cptr value)))
[(eq? v 'comment) comment] (if append?
[(eq? v 'composer) composer] (set-property-cache! key (append (hash-ref key-store (property-symbol key) '()) (list value)))
[(eq? v 'genre) genre] (set-property-cache! key (list value)))]
[(eq? v 'year) year] [else (raise-argument-error who "(or/c string? 'clear)" value)]))
[(eq? v 'track) track]
[(eq? v 'length) length]
[(eq? v 'sample-rate) sample-rate]
[(eq? v 'bit-rate) bit-rate]
[(eq? v 'channels) channels]
[(eq? v 'keys) (hash-keys key-store)]
[(eq? v 'album-artist) album-artist]
[(eq? v 'disc-number) disc-number]
[(eq? v 'val)
(if (null? args)
#f
(hash-ref key-store (car args) #f))]
[(eq? v 'picture) picture]
[(eq? v 'to-hash)
(let ((h (make-hash)))
(hash-set! h 'valid? valid?)
(hash-set! h 'title title)
(hash-set! h 'album album)
(hash-set! h 'artist artist)
(hash-set! h 'comment comment)
(hash-set! h 'composer composer)
(hash-set! h 'genre genre)
(hash-set! h 'year year)
(hash-set! h 'track track)
(hash-set! h 'length length)
(hash-set! h 'sample-rate sample-rate)
(hash-set! h 'bit-rate bit-rate)
(hash-set! h 'channels channels)
(hash-set! h 'picture picture)
(hash-set! h 'keys (hash-keys key-store))
h)]
[else (error (format "Unknown tag-cmd '~a'" v))]
))))
(make-id3-tag-struct handle))
)))
(define (set-values-property! key values)
(ensure-open! 'tags-set-values!)
(cond
[(eq? values clear-tag-value)
(taglib_property_set tag-file (property-name key) #f)
(set-property-cache! key '())]
[(string-list? values)
(taglib_property_set tag-file (property-name key) #f)
(for ([v values]) (taglib_property_set_append tag-file (property-name key) (string->cptr v)))
(set-property-cache! key values)]
[else (raise-argument-error 'tags-set-values! "(or/c (listof string?) 'clear)" values)]))
(define (set-picture! value #:append? [append? #f])
(ensure-open! (if append? 'tags-append-picture! 'tags-picture!))
(cond
[(eq? value clear-tag-value)
(unless (taglib-clear-picture tag-file)
(error 'tags-picture! "could not clear picture for file: ~a" file))
(set! picture #f)]
[(id3-picture? value)
(define ok?
(if append?
(taglib-append-picture tag-file
(id3-picture-mimetype value)
(id3-picture-kind value)
(id3-picture-description value)
(id3-picture-bytes value))
(taglib-set-picture tag-file
(id3-picture-mimetype value)
(id3-picture-kind value)
(id3-picture-description value)
(id3-picture-bytes value))))
(unless ok? (error (if append? 'tags-append-picture! 'tags-picture!)
"could not set picture for file: ~a" file))
(unless append? (set! picture value))]
[else (raise-argument-error (if append? 'tags-append-picture! 'tags-picture!)
"(or/c id3-picture? 'clear)" value)]))
(define (save!)
(ensure-open! 'tags-save!)
(taglib_file_save tag-file))
(define (to-hash)
(let ((h (make-hash)))
(hash-set! h 'valid? valid?)
(hash-set! h 'read-write? read-write?)
(hash-set! h 'closed? closed?)
(hash-set! h 'title title)
(hash-set! h 'album album)
(hash-set! h 'artist artist)
(hash-set! h 'comment comment)
(hash-set! h 'composer composer)
(hash-set! h 'genre genre)
(hash-set! h 'year year)
(hash-set! h 'track track)
(hash-set! h 'length length)
(hash-set! h 'sample-rate sample-rate)
(hash-set! h 'bit-rate bit-rate)
(hash-set! h 'channels channels)
(hash-set! h 'picture picture)
(hash-set! h 'keys (hash-keys key-store))
h))
(define (handle v . args)
(cond
[(eq? v 'valid?) valid?]
[(eq? v 'read-write?) read-write?]
[(eq? v 'closed?) closed?]
[(eq? v 'close!) (close!)]
[(eq? v 'save!) (save!)]
[(eq? v 'title) title]
[(eq? v 'album) album]
[(eq? v 'artist) artist]
[(eq? v 'comment) comment]
[(eq? v 'composer) composer]
[(eq? v 'genre) genre]
[(eq? v 'year) year]
[(eq? v 'track) track]
[(eq? v 'length) length]
[(eq? v 'sample-rate) sample-rate]
[(eq? v 'bit-rate) bit-rate]
[(eq? v 'channels) channels]
[(eq? v 'keys) (hash-keys key-store)]
[(eq? v 'album-artist) album-artist]
[(eq? v 'disc-number) disc-number]
[(eq? v 'val) (if (null? args) #f (hash-ref key-store (property-symbol (car args)) #f))]
[(eq? v 'picture) picture]
[(eq? v 'to-hash) (to-hash)]
[(eq? v 'set-title!) (apply-string! 'tags-title! (car args) taglib_tag_set_title (lambda (x) (set! title x)))]
[(eq? v 'set-album!) (apply-string! 'tags-album! (car args) taglib_tag_set_album (lambda (x) (set! album x)))]
[(eq? v 'set-artist!) (apply-string! 'tags-artist! (car args) taglib_tag_set_artist (lambda (x) (set! artist x)))]
[(eq? v 'set-comment!) (apply-string! 'tags-comment! (car args) taglib_tag_set_comment (lambda (x) (set! comment x)))]
[(eq? v 'set-genre!) (apply-string! 'tags-genre! (car args) taglib_tag_set_genre (lambda (x) (set! genre x)))]
[(eq? v 'set-year!) (apply-uint! 'tags-year! (car args) taglib_tag_set_year (lambda (x) (set! year x)))]
[(eq? v 'set-track!) (apply-uint! 'tags-track! (car args) taglib_tag_set_track (lambda (x) (set! track x)))]
[(eq? v 'set-composer!) (set-one-property! 'tags-composer! 'composer (car args))]
[(eq? v 'set-album-artist!) (set-one-property! 'tags-album-artist! 'albumartist (car args))]
[(eq? v 'set-disc-number!)
(let ((x (car args)))
(cond
[(eq? x clear-tag-value) (set-one-property! 'tags-disc-number! 'discnumber clear-tag-value)]
[(and (exact-nonnegative-integer? x) (<= x #xffffffff)) (set-one-property! 'tags-disc-number! 'discnumber (number->string x))]
[(string? x) (set-one-property! 'tags-disc-number! 'discnumber x)]
[else (raise-argument-error 'tags-disc-number! "(or/c exact-nonnegative-integer? string? 'clear)" x)]))]
[(eq? v 'set!) (set-one-property! 'tags-set! (car args) (cadr args))]
[(eq? v 'set-values!) (set-values-property! (car args) (cadr args))]
[(eq? v 'append!) (set-one-property! 'tags-append! (car args) (cadr args) #:append? #t)]
[(eq? v 'clear!) (set-one-property! 'tags-clear! (car args) clear-tag-value)]
[(eq? v 'set-picture!) (set-picture! (car args))]
[(eq? v 'append-picture!) (set-picture! (car args) #:append? #t)]
[(eq? v 'clear-picture!) (set-picture! clear-tag-value)]
[else (error (format "Unknown tag-cmd '~a'" v))]))
(open-and-read!)
(make-id3-tag-struct handle))
(define (call-with-id3-tags file proc #:mode [mode 'read])
(define tags (id3-tags file #:mode mode))
(dynamic-wind
void
(lambda () (proc tags))
(lambda () (tags-close! tags))))
(define-syntax def (define-syntax def
(syntax-rules () (syntax-rules ()
((_ (fun v)) ((_ (fun v))
(define (fun tags . args) (define (fun tags . args)
(apply (id3-tag-struct-handle tags) (cons v args))) (apply (id3-tag-struct-handle tags) (cons v args))))))
)))
(define-syntax defs (define-syntax defs
(syntax-rules () (syntax-rules ()
((_ f1) ((_ f1) (def f1))
(def f1)) ((_ f1 f2 ...) (begin (def f1) (def f2) ...))))
((_ f1 f2 ...)
(begin
(def f1)
(def f2)
...))
))
(defs (defs
(tags-valid? 'valid?) (tags-valid? 'valid?)
(tags-read-write? 'read-write?)
(tags-closed? 'closed?)
(tags-close! 'close!)
(tags-save! 'save!)
(tags-title 'title) (tags-title 'title)
(tags-album 'album) (tags-album 'album)
(tags-artist 'artist) (tags-artist 'artist)
@@ -234,6 +564,17 @@
(tags-year 'year) (tags-year 'year)
(tags-track 'track) (tags-track 'track)
(tags-title! 'set-title!)
(tags-album! 'set-album!)
(tags-artist! 'set-artist!)
(tags-comment! 'set-comment!)
(tags-genre! 'set-genre!)
(tags-composer! 'set-composer!)
(tags-album-artist! 'set-album-artist!)
(tags-disc-number! 'set-disc-number!)
(tags-year! 'set-year!)
(tags-track! 'set-track!)
(tags-length 'length) (tags-length 'length)
(tags-sample-rate 'sample-rate) (tags-sample-rate 'sample-rate)
(tags-bit-rate 'bit-rate) (tags-bit-rate 'bit-rate)
@@ -241,10 +582,16 @@
(tags-keys 'keys) (tags-keys 'keys)
(tags-ref 'val) (tags-ref 'val)
(tags-set! 'set!)
(tags-set-values! 'set-values!)
(tags-append! 'append!)
(tags-clear! 'clear!)
(tags-picture 'picture) (tags-picture 'picture)
(tags->hash 'to-hash) (tags-picture! 'set-picture!)
) (tags-append-picture! 'append-picture!)
(tags-clear-picture! 'clear-picture!)
(tags->hash 'to-hash))
(define (tags-picture->bitmap tags) (define (tags-picture->bitmap tags)
(let ((p (tags-picture tags))) (let ((p (tags-picture tags)))
@@ -257,34 +604,27 @@
(define (tags-picture->kind tags) (define (tags-picture->kind tags)
(let ((p (tags-picture tags))) (let ((p (tags-picture tags)))
(if (eq? p #f) (if (eq? p #f) #f (id3-picture-kind p))))
#f
(id3-picture-kind p))))
(define (tags-picture->mimetype tags) (define (tags-picture->mimetype tags)
(let ((p (tags-picture tags))) (let ((p (tags-picture tags)))
(if (eq? p #f) (if (eq? p #f) #f (id3-picture-mimetype p))))
#f
(id3-picture-mimetype p)))) (define (tags-picture->description tags)
(let ((p (tags-picture tags)))
(if (eq? p #f) #f (id3-picture-description p))))
(define (tags-picture->ext tags) (define (tags-picture->ext tags)
(let ((mt (tags-picture->mimetype tags))) (let ((mt (tags-picture->mimetype tags)))
(cond (cond
((eq? mt #f) [(eq? mt #f) #f]
#f) [(or (string-suffix? mt "/jpeg") (string-suffix? mt "/jpg")) 'jpg]
((or (string-suffix? mt "/jpeg") (string-suffix? mt "/jpg")) [(string-suffix? mt "/png") 'png]
'jpg) [else #f])))
((string-suffix? mt "/png")
'png)
(else #f)
)
))
(define (tags-picture->size tags) (define (tags-picture->size tags)
(let ((p (tags-picture tags))) (let ((p (tags-picture tags)))
(if (eq? p #f) (if (eq? p #f) #f (id3-picture-size p))))
#f
(id3-picture-size p))))
(define (tags-picture->file tags path) (define (tags-picture->file tags path)
(let ((p (tags-picture tags))) (let ((p (tags-picture tags)))
@@ -300,6 +640,4 @@
(close-input-port in) (close-input-port in)
#t)))) #t))))
)
); end of module
+1 -1
View File
@@ -21,7 +21,7 @@
(define-runtime-path tests "../racket-audio-test") (define-runtime-path tests "../racket-audio-test")
(define test-file1 (build-path tests "idyll.mp3")) (define test-file1 (build-path tests "idyll.mp3"))
(define test-file2 (build-path tests "idyll.flac")) (define test-file2 (build-path tests "idyll.opus"))
(define test-file3 (build-path tests "mahler-1.mp3")) (define test-file3 (build-path tests "mahler-1.mp3"))
(define test-file4 (build-path tests "mahler-2.mp3")) (define test-file4 (build-path tests "mahler-2.mp3"))
(define test-file5 (build-path tests "mahler-1.opus")) (define test-file5 (build-path tests "mahler-1.opus"))
+32
View File
@@ -0,0 +1,32 @@
#lang racket/base
(require rackunit
racket-audio)
(define formats
(audio-supported-formats))
(check-not-false
(member "flac"
(audio-supported-extensions)))
(check-equal?
(cdr (assoc "flac" formats))
"audio/x-flac")
(check-eq?
(audio-decoder-for-extension "flac")
'flac)
(check-eq?
(audio-decoder-for-extension ".mp3")
'ffmpeg)
(check-not-false
(member (audio-decoder-for-extension 'opus)
'(opusfile ffmpeg)))
(check-false
(audio-decoder-for-extension "not-audio"))
(for ((extension
(in-list
(audio-supported-extensions))))
(check-true
(symbol?
(audio-decoder-for-extension extension))))
+69
View File
@@ -0,0 +1,69 @@
#lang racket/base
(require "../flac-decoder.rkt")
(define (test-flac-thread flac-path #:pool [pool #f])
(define result-ch (make-channel))
(define (start)
(thread
(lambda ()
(with-handlers ([exn:fail?
(lambda (e)
(channel-put result-ch
(list 'error (exn-message e))))])
(define frames 0)
(define buffers 0)
(define bytes 0)
(define fmt #f)
(define h
(flac-open
flac-path
;; stream-info callback
(lambda (info)
(set! fmt info)
(printf "format: ~s\n" info)
(flush-output))
;; audio callback
(lambda (info buffer len)
(set! buffers (add1 buffers))
(set! bytes (+ bytes len))
(define blocksize (hash-ref info 'blocksize #f))
(when (integer? blocksize)
(set! frames (+ frames blocksize)))
;; af en toe iets printen, zodat je ziet dat hij loopt
(when (zero? (modulo buffers 500))
(printf "buffers=~a bytes=~a frames=~a\n"
buffers bytes frames)
(flush-output)))))
(unless h
(error 'test-flac-thread "could not open FLAC file: ~a" flac-path))
(define state (flac-read h))
(channel-put result-ch
(list 'ok
'state state
'buffers buffers
'bytes bytes
'frames frames
'format fmt))))
#:pool pool)
)
(start)
(start)
(start)
(channel-get result-ch)
(channel-get result-ch)
(channel-get result-ch)
)
; (test-flac-thread "\\\\panderleou\\music\\Jazz\\LA4\\LA4 - Zaca\\01 Zaca.flac")
+46
View File
@@ -0,0 +1,46 @@
#lang racket/base
(require "../main.rkt"
"../audio-encoder.rkt")
(define (convert-to-opus flac-in opus-out)
(let* ((opus-kbps 224)
(settings (hash 'bitrate (* opus-kbps 1000)
'vbr #t))
)
(with-handlers ([exn? (λ (e)
(displayln (format "~a" e))
(when (file-exists? opus-out)
(delete-file opus-out))
#f)])
(let ((result (audio-encode flac-in opus-out
settings
#:encoder 'opus
#:copy-tags? #t)))
#t)
)
)
)
(define (times-3-convert flac-in opus-out1 opus-out2 opus-out3 #:pool [pool #f])
(define (start opus-out)
(thread (λ ()
(displayln (format "Starting conversion: ~a" opus-out))
(convert-to-opus flac-in opus-out)
(displayln (format" converted: ~a" opus-out))
#t
) #:pool pool)
)
(let* ((t1 (start opus-out1))
(t2 (start opus-out2))
(t3 (start opus-out3))
)
(thread-wait t1)
(thread-wait t2)
(thread-wait t3)
)
)
; (convert-to-opus "\\\\panderleou\\music\\Jazz\\LA4\\LA4 - Zaca\\01 Zaca.flac" "c:\\tmp\\test.opus" )