Remote usage of audio-placed-player.rkt

This commit is contained in:
2026-06-08 15:46:27 +02:00
parent 17846e068c
commit 6ed566c6cd
9 changed files with 624 additions and 91 deletions
+91
View File
@@ -29,6 +29,26 @@ uses libFLAC directly. FLAC sample-rate conversion uses the existing FFmpeg
swresample layer. 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 \
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:
@@ -46,6 +66,19 @@ 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-1/windows-x86_64.zip
```
The archive is installed below Racket's addon directory by
`download-soundlibs`.
## Encoder examples
Encode to Opus:
@@ -78,3 +111,61 @@ A small test wrapper is available in `encoder-test.rkt`:
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. On Unix-like systems the SSH
launcher defaults to `ssh -T -q`. On Windows it prefers PuTTY `plink.exe` or
`plink` with `-batch -T`, and falls back to OpenSSH `ssh.exe`/`ssh` when PuTTY
is not present. These defaults can be overridden with the `#:ssh-program`,
`#:ssh-options`, `#:remote-racket`, `#:remote-module`, and `#:remote-command`
arguments to `make-audio-player`.
Example:
```racket
(define player
(make-audio-player cb-state cb-eof
#:remote-host "nas"
#:remote-path-map
(list (list "/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`.
+57 -16
View File
@@ -1,7 +1,8 @@
#lang racket/base
(require racket/place
racket/async-channel
(require racket/port
port-channel
uni-channel
"libao.rkt"
"audio-decoder.rkt"
"private/utils.rkt"
@@ -9,19 +10,43 @@
)
(provide placed-player
placed-player/stdio
audio-known-exts?
)
(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)
(let ((s1* (inexact->exact (round s1)))
(s2* (inexact->exact (round s2))))
(= s1* s2*)))
(define (placed-player ch-in)
(let ((ch-evt #f)
(ch-out #f)
(define (placed-player ch-in [initial-ch-out #f] [initial-ch-evt #f])
(let ((ch-evt initial-ch-evt)
(ch-out initial-ch-out)
(ao-h #f)
(ao-mutex (make-mutex))
(ao-dec #f)
@@ -53,20 +78,25 @@
(begin b1 ...)
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)
(if (place-channel? ch-out)
(place-channel-put ch-out data)
(async-channel-put ch-out data)))
(uni-channel-put ch-out data))
(define (evt data)
(if (place-channel? ch-evt)
(place-channel-put ch-evt data)
(async-channel-put ch-evt data)))
(uni-channel-put ch-evt data))
(define (get)
(if (place-channel? ch-in)
(place-channel-get ch-in)
(async-channel-get ch-in)))
(uni-channel-get ch-in))
(define (audio-read-worker ao-dec file-id)
(set! feeding-audio #t)
@@ -456,8 +486,8 @@
(state "quit" evt 'force)
'(quit)))
((eq? cmd 'init) (do-rpc
(set! ch-out (cadr data))
(set! ch-evt (caddr data))
(set! ch-out (->uni-channel (cadr data)))
(set! ch-evt (->uni-channel (caddr data)))
'(initialized))
(loop))
(else
@@ -537,3 +567,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)))
+96 -36
View File
@@ -4,8 +4,10 @@
racket/contract
racket/async-channel
racket/runtime-path
uni-channel
"audio-placed-player.rkt"
"private/utils.rkt"
"private/remote-utils.rkt"
(prefix-in ffi: ffi/unsafe)
)
@@ -35,12 +37,22 @@
audio-known-exts?
audio-param!
audio-param
audio-remote-path
racket-sound-default-ssh-program
racket-sound-default-ssh-options
racket-sound-default-remote-racket
racket-sound-default-remote-module
racket-sound-default-remote-command
current-racket-sound-ssh-program
current-racket-sound-remote-racket
current-racket-sound-remote-module
)
(define-runtime-path placed-player-module "audio-placed-player.rkt")
(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 remote-path-map)
#:mutable
#:transparent
)
@@ -88,8 +100,24 @@
(unless cond (error (format message ...))))))
(define/contract (make-audio-player cb-state cb-eof-stream
#:use-place [use-place (place-enabled?)])
(->* (procedure? procedure?) (#:use-place boolean?) audio-play?)
#:use-place [use-place (place-enabled?)]
#:remote-host [remote-host #f]
#:remote-path-map [remote-path-map '()]
#:remote-racket [remote-racket (current-racket-sound-remote-racket)]
#:remote-module [remote-module (current-racket-sound-remote-module)]
#:remote-command [remote-command #f]
#:ssh-program [ssh-program (current-racket-sound-ssh-program)]
#:ssh-options [ssh-options #f])
(->* (procedure? procedure?)
(#:use-place boolean?
#:remote-host (or/c #f string?)
#:remote-path-map remote-path-map?
#:remote-racket path-string?
#:remote-module string?
#:remote-command (or/c #f (listof string?))
#:ssh-program path-string?
#:ssh-options (or/c #f (listof string?)))
audio-play?)
(let ((cmd-ch #f)
(ret-ch #f)
(evt-ch #f)
@@ -101,38 +129,69 @@
(rpc #f)
(rpc-mutex (make-mutex))
)
(if use-place
(begin
(set! cmd-ch (dynamic-place placed-player-module 'placed-player))
(set! cmd-put (λ (data) (place-channel-put cmd-ch data)))
(set! au-pl cmd-ch)
(set! dead-guard (λ () (let ((evt (place-dead-evt au-pl)))
(sync evt))))
(let-values (((ret-ch-in ret-ch-out) (place-channel))
((evt-ch-in evt-ch-out) (place-channel)))
(place-channel-put cmd-ch (list 'init ret-ch-out evt-ch-out))
(set! evt-ch evt-ch-in)
(set! ret-ch ret-ch-in)
(assert (is-return? (place-channel-get ret-ch-in) 'initialized)
"Unexpected: not 'initialized returnd from 'init command"))
)
(begin
(set! cmd-ch (make-async-channel))
(set! cmd-put (λ (data) (async-channel-put cmd-ch data)))
(set! au-pl (thread (λ () (placed-player cmd-ch))))
(set! dead-guard (λ () (let ((evt (thread-dead-evt au-pl)))
(sync evt))))
(set! ret-ch (make-async-channel))
(set! evt-ch (make-async-channel))
(async-channel-put cmd-ch (list 'init ret-ch evt-ch))
(assert (is-return? (async-channel-get ret-ch) 'initialized)
"Unexpected: not 'initialized returnd from 'init command")
)
)
(set! ret-get (λ () (to-ret-value (sync ret-ch))))
(set! evt-get (λ (timeout-ms) (sync/timeout (/ timeout-ms 1000) evt-ch)))
(set! rpc (λ (cmd . args) (with-mutex rpc-mutex
(cmd-put (cons cmd args)) (ret-get))))
(cond
[remote-host
;; Remote mode starts a worker over ssh. The remote worker uses
;; placed-player/stdio, so the existing three logical channels map to
;; ssh stdin, stdout and stderr. No init command is sent in this mode:
;; the worker starts with all three channels already supplied.
(let ((cmd (or remote-command (racket-sound-default-remote-command remote-racket remote-module))))
(let-values (((cmd-ch* ret-ch* evt-ch* proc dead-guard*)
(start-remote-placed-player remote-host
#:ssh-program ssh-program
#:ssh-options ssh-options
#:remote-command cmd)))
(set! cmd-ch cmd-ch*)
(set! ret-ch ret-ch*)
(set! evt-ch evt-ch*)
(set! au-pl proc)
(set! dead-guard dead-guard*)))]
[use-place
;; dynamic-place returns the command place-channel. The raw channel
;; is kept for place-dead-evt, while normal traffic is sent through
;; a uni-channel wrapper.
(let ((raw-cmd-ch (dynamic-place placed-player-module 'placed-player)))
(set! cmd-ch (make-uni-channel raw-cmd-ch))
(set! au-pl raw-cmd-ch)
(set! dead-guard (lambda () (let ((evt (place-dead-evt au-pl)))
(sync evt))))
(let-values (((ret-ch-in ret-ch-out) (place-channel))
((evt-ch-in evt-ch-out) (place-channel)))
;; Do not send uni-channel structs through a place-channel: they
;; contain procedures and are not place-message values. Send the
;; 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))
(uni-channel-put cmd-ch (list 'init ret-ch-out evt-ch-out))
(assert (is-return? (uni-channel-get ret-ch) 'initialized)
"Unexpected: not 'initialized returned from 'init command")))]
[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 (audio-remote-path (car args) remote-path-map) (cdr args))
args))
(cmd-put (cons cmd args*))
(ret-get))))
(let* ((handle #f)
(cb-state* (λ (st st-hash) (cb-state handle st st-hash)))
@@ -142,7 +201,8 @@
rpc
au-pl
#f
(make-hash)))
(make-hash)
remote-path-map))
(set-audio-play-evt-thread! handle
(thread
(λ ()
+1
View File
@@ -16,6 +16,7 @@
"finalizer" "draw-lib" "net-lib"
"simple-log" "racket-sprintf"
"early-return" "let-assert"
"uni-channel" "port-channel"
"rackunit-lib"
)
)
+5 -5
View File
@@ -26,8 +26,8 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define version-major 1)
(define version-minor 0)
(define version-patch 0)
(define version-minor 1)
(define version-patch 1)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Internal functions
@@ -40,8 +40,8 @@
version-patch
))
(define download-site "git.dijkewijk.nl")
(define base-path "hans/racket-sound-lib/releases/download")
(define download-site "codeberg.org")
(define base-path "hnmdijkema/racket-sound-lib/releases/download")
(define os (system-type 'os*))
(define arch (system-type 'arch))
@@ -145,7 +145,7 @@
(let* ((file (build-path install-path "archive.zip"))
(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)
(displayln (format "downloaded '~a'" file))
(when (directory-exists? ffi-path)
+153
View File
@@ -0,0 +1,153 @@
#lang racket/base
(require racket/list
racket/path
racket/string
racket/system
port-channel
uni-channel)
(provide remote-path-map?
audio-remote-path
racket-sound-default-ssh-program
racket-sound-default-ssh-options
racket-sound-default-remote-racket
racket-sound-default-remote-module
racket-sound-default-remote-command
racket-sound-resolve-executable
racket-sound-start-ssh-subprocess
current-racket-sound-ssh-program
current-racket-sound-remote-racket
current-racket-sound-remote-module
make-remote-port-uni-channels
start-remote-placed-player)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Remote path mapping
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (path-string->string p)
(if (path? p) (path->string p) p))
(define (remote-path-map? v)
(or (not v)
(procedure? v)
(and (list? v)
(andmap (lambda (entry)
(cond [(and (pair? entry) (pair? (cdr entry)) (null? (cddr entry)))
(and (path-string? (car entry)) (path-string? (cadr entry)))]
[(and (pair? entry) (path-string? (car entry)) (path-string? (cdr entry))) #t]
[(vector? entry)
(and (= (vector-length entry) 2)
(path-string? (vector-ref entry 0))
(path-string? (vector-ref entry 1)))]
[else #f]))
v))))
(define (path-map-entry-local entry)
(path-string->string
(cond [(vector? entry) (vector-ref entry 0)]
[(and (pair? entry) (pair? (cdr entry)) (null? (cddr entry))) (car entry)]
[else (car entry)])))
(define (path-map-entry-remote entry)
(path-string->string
(cond [(vector? entry) (vector-ref entry 1)]
[(and (pair? entry) (pair? (cdr entry)) (null? (cddr entry))) (cadr entry)]
[else (cdr entry)])))
(define (replace-prefix s from to)
(string-append to (substring s (string-length from))))
(define (audio-remote-path path map)
(define s (path-string->string path))
(cond [(not map) s]
[(procedure? map) (map s)]
[(null? map) s]
[else
(define matches
(filter (lambda (entry) (string-prefix? s (path-map-entry-local entry))) map))
(cond [(null? matches) s]
[else
(define best
(argmax (lambda (entry) (string-length (path-map-entry-local entry))) matches))
(replace-prefix s (path-map-entry-local best) (path-map-entry-remote best))])]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SSH / subprocess defaults
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(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 (racket-sound-default-ssh-program)
(cond [(eq? (system-type 'os) 'windows)
(cond [(find-executable-path "plink.exe") => values]
[(find-executable-path "plink") => values]
[(find-executable-path "ssh.exe") => values]
[(find-executable-path "ssh") => values]
[else "plink.exe"])]
[else
(cond [(find-executable-path "ssh") => values]
[else "ssh"])]))
(define (racket-sound-default-ssh-options [ssh-program (racket-sound-default-ssh-program)])
(if (plink-program? ssh-program)
'("-batch" "-T")
'("-T" "-q")))
(define (racket-sound-default-remote-racket) "racket")
(define (racket-sound-default-remote-module) "racket-audio/audio-placed-player")
(define (racket-sound-default-remote-command
[remote-racket (racket-sound-default-remote-racket)]
[remote-module (racket-sound-default-remote-module)])
(list remote-racket "-l" remote-module "--" "--stdio"))
(define current-racket-sound-ssh-program
(make-parameter (racket-sound-default-ssh-program)))
(define current-racket-sound-remote-racket
(make-parameter (racket-sound-default-remote-racket)))
(define current-racket-sound-remote-module
(make-parameter (racket-sound-default-remote-module)))
(define (racket-sound-resolve-executable p)
(or (find-executable-path p) p))
(define (racket-sound-start-ssh-subprocess remote-host remote-command
#:ssh-program [ssh-program (current-racket-sound-ssh-program)]
#:ssh-options [ssh-options #f])
(define ssh-options* (or ssh-options (racket-sound-default-ssh-options ssh-program)))
(define args (append ssh-options* (list remote-host) remote-command))
(apply subprocess #f #f #f (racket-sound-resolve-executable ssh-program) args))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Port-channel wrapping
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (make-port-uc port direction source)
(make-uni-channel (make-port-channel port #:direction direction #:source source #:close? #t)))
(define (make-remote-port-uni-channels stdout stdin stderr)
(values (make-port-uc stdin 'output 'remote-stdin)
(make-port-uc stdout 'input 'remote-stdout)
(make-port-uc stderr 'input 'remote-stderr)))
(define (start-remote-placed-player remote-host
#:ssh-program [ssh-program (current-racket-sound-ssh-program)]
#:ssh-options [ssh-options #f]
#:remote-command [remote-command (racket-sound-default-remote-command)])
(define-values (proc stdout stdin stderr)
(racket-sound-start-ssh-subprocess remote-host remote-command
#:ssh-program ssh-program
#:ssh-options ssh-options))
(define-values (cmd-ch ret-ch evt-ch)
(make-remote-port-uni-channels stdout stdin stderr))
(define dead-guard (lambda () (subprocess-wait proc)))
(values cmd-ch ret-ch evt-ch proc dead-guard))
+72 -15
View File
@@ -1,7 +1,9 @@
(module utils racket/base
(require racket/path
racket/file
racket/runtime-path
racket/system
ffi/unsafe
setup/dirs
"downloader.rkt"
@@ -19,6 +21,9 @@
warn-sound
fatal-sound
sync-log-sound
racket-sound-cache-directory
racket-sound-log-file
open-racket-sound-log-file
integer->int-bytes
int-bytes->integer
valid-ffmpeg-versions
@@ -34,6 +39,30 @@
(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
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -135,16 +164,16 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define valid-ffmpeg-versions
(make-hash (list (list 'avutil 58 60 "libavcodec")
(list 'avcodec 60 62 "libavutil")
(list 'avformat 60 62 "libswresample")
(list 'swresample 4 6 "libavformat")
(make-hash (list (list 'avutil 58 60 "libavutil")
(list 'avcodec 60 62 "libavcodec")
(list 'avformat 60 62 "libavformat")
(list 'swresample 4 6 "libswresample")
))
)
(define (version-str 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.so.~a - ~a.so.~a\n" (caddr v) (car v) (caddr v) (cadr v))
)
)
@@ -162,22 +191,50 @@
"Make sure you have installed the following libraries,\n"
"e.g. on a debian based system with apt:\n"
"\n"
" FLAC : sudo apt install libflac12\n"
" mpg123 : libmpg123-0\n"
" libao : libao4\n"
" ffmpeg : libavcodec60 libavutil58 libswresample4 libavformat60\n"
"\n"
" sudo apt install libflac12 libmpg123-0 libao4 \
"
" libavcodec60 libavutil58 libswresample4 libavformat60 \
"
" libogg0 libopus0 libopusenc0 libopusfile0 libtag1v5
"
"
"
"For development from source or local FFI rebuilding, install the matching -dev packages,
"
"for example libflac-dev, libmpg123-dev, libao-dev, libavcodec-dev,
"
"libavutil-dev, libswresample-dev, libavformat-dev, libogg-dev,
"
"libopus-dev, libopusenc-dev, libopusfile-dev and libtag1-dev.
"
"
"
)))
((eq? st 'macosx)
(displayln
(string-append
"Make sure you have the right libraries installed, using 'homebrew', see https://brew.sh/\n"
"\n"
" brew install ffmpeg-full\n"
" brew install libao\n"
" brew install mpg123\n"
" brew install flac\n"
"\n"
" brew install ffmpeg
"
" brew install libao
"
" brew install mpg123
"
" brew install flac
"
" brew install opus
"
" brew install libopusenc
"
" brew install taglib
"
"
"
"If your local setup uses ffmpeg-full instead of ffmpeg, install that variant instead.
"
"
"
)))
(else
(displayln
+60 -16
View File
@@ -5,6 +5,8 @@
racket/contract
racket/place
racket/async-channel
uni-channel
port-channel
"../audio-placed-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
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
normal Racket thread with async channels. That mode is useful for debugging,
because the player then stays in the same process and can be inspected more
easily.
normal Racket thread with async channels. Both modes are mediated through
@racketmodname[uni-channel]: each of the existing logical channels is wrapped as
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
isolated from scheduling delays in the main Racket VM, such as GUI activity,
@@ -40,22 +44,58 @@ debugging, or interaction with DrRacket.
@section{Interface}
@defproc[(placed-player [ch-in (or/c place-channel? async-channel?)]) void?]{
Runs the placed-player command loop on @racket[ch-in]. The channel may be a
place channel or an async channel. The command loop receives list commands,
@defproc[(placed-player
[ch-in (or/c uni-channel? place-channel? async-channel? port-channel?)]
[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
until it receives @racket['quit].
The function is designed to be started either by @racket[dynamic-place] or by
@racket[thread]. In place mode, all three channels are place channels. In
thread mode, all three channels are async channels. The implementation detects
the kind of channel and uses @racket[place-channel-put],
@racket[place-channel-get], @racket[async-channel-put], or
@racket[async-channel-get] as appropriate.}
When @racket[ch-out] and @racket[ch-evt] are @racket[#f], the command loop
expects an initial @racket['init] command containing the raw reply and event
channels. This remains the normal path for @racket[dynamic-place], because a
@racket[uni-channel] value contains procedures and must not be sent through a
place channel. In that case the controlling side sends the raw place channels
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,
sends the initial @racket['init] command, starts an event thread, and exposes a
contracted API. The placed player itself only exports @racket[placed-player].
sends the initial @racket['init] command when needed, starts an event thread, and exposes a
contracted API. The placed player exports @racket[placed-player] and the
standard-port worker entry point @racket[placed-player/stdio].
@section{Overall state model}
@@ -91,7 +131,10 @@ installed.
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
@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
@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}
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
same.
+88 -2
View File
@@ -28,7 +28,14 @@ through callbacks supplied when the player is created.
@defproc[(make-audio-player
[cb-state procedure?]
[cb-eof-stream procedure?]
[#:use-place use-place boolean?])
[#:use-place use-place boolean?]
[#:remote-host remote-host (or/c #f string?) #f]
[#:remote-path-map remote-path-map any/c '()]
[#:remote-racket remote-racket path-string? "racket"]
[#:remote-module remote-module string? "racket-audio/audio-placed-player"]
[#:remote-command remote-command (or/c #f (listof string?)) #f]
[#:ssh-program ssh-program path-string? (current-racket-sound-ssh-program)]
[#:ssh-options ssh-options (or/c #f (listof string?)) #f])
audio-play?]{
Creates an audio player and returns a player handle. The handle is passed to
all other procedures in this module.
@@ -108,8 +115,87 @@ a separate Racket VM, so decoding and buffer feeding are less exposed to
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
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. The remote worker
is expected to run @racket[placed-player/stdio], where stdin is the command
channel, stdout is the reply channel, and stderr is the event channel. The
client side wraps those three process ports through @racketmodname[port-channel]
and @racketmodname[uni-channel]. The default command is equivalent to:
@racketblock[
(list remote-racket "-l" remote-module "--" "--stdio")]
The remote launcher defaults are supplied by the remote utility layer. On Unix-like systems @racket[ssh] is used with @racket['("-T" "-q")]. On Windows the launcher first looks for PuTTY @tt{plink.exe} or @tt{plink}; if found, the default options are @racket['("-batch" "-T")]. If @tt{plink} is not found, the launcher falls back to OpenSSH @tt{ssh.exe}/@tt{ssh}. The default remote command is equivalent to:
@racketblock[
(racket-sound-default-remote-command remote-racket remote-module)]
When @racket[ssh-options] is @racket[#f], suitable options are derived from the selected SSH program. If the remote setup needs a different launcher command, provide @racket[remote-command] as a list of command-line words.
The remote player must be able to open the requested audio files. When the
local and remote file trees differ, use @racket[remote-path-map]. It may be a
procedure from path string to path string, or a list of mappings. Each mapping
may be a two-element list, a cons pair, or a two-element vector. The longest
matching local prefix is replaced by the corresponding remote prefix before the
@racket['open] command is sent. For example:
@racketblock[
(make-audio-player cb-state cb-eof
#:remote-host "nas"
#:remote-path-map
(list (list "/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[(audio-remote-path [path path-string?]
[remote-path-map any/c])
string?]{
Applies the same path translation used by remote playback. This is primarily a
small helper for testing SSH path-map configuration before starting playback.}
@section[#:tag "audio-player-remote-defaults"]{Remote defaults}
@defproc[(racket-sound-default-ssh-program) path-string?]{
Returns the default SSH client for remote playback. On Windows this prefers
PuTTY @tt{plink.exe}/@tt{plink}, then OpenSSH @tt{ssh.exe}/@tt{ssh}. On other
platforms it uses @tt{ssh}.}
@defproc[(racket-sound-default-ssh-options [ssh-program path-string?])
(listof string?)]{
Returns default command-line options for @racket[ssh-program]. For @tt{plink}
this is @racket['("-batch" "-T")]; for OpenSSH this is @racket['("-T" "-q")].}
@defproc[(racket-sound-default-remote-racket) string?]{
Returns the default remote Racket executable name, currently @racket["racket"].}
@defproc[(racket-sound-default-remote-module) string?]{
Returns the default remote module, currently
@racket["racket-audio/audio-placed-player"].}
@defproc[(racket-sound-default-remote-command
[remote-racket path-string? (racket-sound-default-remote-racket)]
[remote-module string? (racket-sound-default-remote-module)])
(listof string?)]{
Builds the default remote worker command:
@racketblock[
(list remote-racket "-l" remote-module "--" "--stdio")]
}
@defthing[current-racket-sound-ssh-program parameter?]{
Parameter holding the default SSH program used by @racket[make-audio-player]
when @racket[#:ssh-program] is not supplied.}
@defthing[current-racket-sound-remote-racket parameter?]{
Parameter holding the default remote Racket executable name.}
@defthing[current-racket-sound-remote-module parameter?]{
Parameter holding the default remote module name.}
@defproc[(audio-play? [v any/c]) boolean?]{
Returns @racket[#t] when @racket[v] is a currently valid audio player handle.