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
+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.