jmri_core.beans.audio

Audio — a JMRI audio bean (source / buffer / listener).

JMRI's audio subsystem in Java wraps JOAL/OpenAL, but the JSON contract is a thin bean shape: name, comment, state (16 STOPPED / 17 PLAYING), plus per-kind properties (a URL for a buffer, a gain for a source).

We ship the bean + manager here; the JSON service in jmri_server.services.audio exposes list/get/put/post/delete over the wire with the Play / Stop commands JMRI mobile clients issue. Actually playing audio is out of scope — this pass gives wire parity so scripts and mobile clients can drive audio state and react to state pushes.

 1"""``Audio`` — a JMRI audio bean (source / buffer / listener).
 2
 3JMRI's audio subsystem in Java wraps JOAL/OpenAL, but the JSON contract
 4is a thin bean shape: name, comment, state (16 STOPPED / 17 PLAYING),
 5plus per-kind properties (a URL for a buffer, a gain for a source).
 6
 7We ship the bean + manager here; the JSON service in
 8``jmri_server.services.audio`` exposes list/get/put/post/delete over
 9the wire with the Play / Stop commands JMRI mobile clients issue.
10Actually *playing* audio is out of scope — this pass gives wire
11parity so scripts and mobile clients can drive audio state and
12react to state pushes.
13"""
14from __future__ import annotations
15
16from typing import Literal
17
18from jmri_core.bean import AbstractNamedBean
19from jmri_core.manager import Manager
20
21# JMRI Java's Audio.getState() returns these two numbers on audio
22# sources — checked against ``jmri.server.json.audio.JsonAudioHttpService``
23# (case 16 = STOPPED, case 17 = PLAYING). The stage-0 schema lists
24# ``[0, 2, 4, 8]``; that predates the state-enum revision and doesn't
25# reflect what live JMRI Java actually emits. We follow the code.
26AUDIO_STATE_STOPPED = 16
27AUDIO_STATE_PLAYING = 17
28
29AudioKind = Literal["source", "buffer", "listener"]
30
31
32class Audio(AbstractNamedBean):
33    """One audio object. Kind selects between the three JMRI audio roles."""
34
35    __slots__ = ("_state", "gain", "kind", "url")
36
37    type_letter = "A"
38
39    def __init__(
40        self, system_name: str, user_name: str | None = None,
41    ) -> None:
42        super().__init__(system_name, user_name)
43        self._state: int = AUDIO_STATE_STOPPED
44        self.kind: AudioKind = "source"
45        # Source: URL of a buffer to play. Buffer: URL of the sample file.
46        # Listener: unused. All are opaque strings for the JSON layer.
47        self.url: str | None = None
48        # Source gain 0..1. Ignored for buffer / listener.
49        self.gain: float = 1.0
50
51    @property
52    def state(self) -> int:
53        return self._state
54
55    async def play(self) -> None:
56        """Mark this source as playing; emits ``state_changed``."""
57        if self._state == AUDIO_STATE_PLAYING:
58            return
59        self._state = AUDIO_STATE_PLAYING
60        await self.state_changed.emit(self._state)
61
62    async def stop(self) -> None:
63        """Mark this source as stopped; emits ``state_changed``."""
64        if self._state == AUDIO_STATE_STOPPED:
65            return
66        self._state = AUDIO_STATE_STOPPED
67        await self.state_changed.emit(self._state)
68
69
70class AudioManager(Manager[Audio]):
71    type_letter = "A"
72
73    def _create(self, system_name: str) -> Audio:
74        return Audio(system_name)
AUDIO_STATE_STOPPED = 16
AUDIO_STATE_PLAYING = 17
AudioKind = typing.Literal['source', 'buffer', 'listener']
class Audio(jmri_core.bean.AbstractNamedBean):
33class Audio(AbstractNamedBean):
34    """One audio object. Kind selects between the three JMRI audio roles."""
35
36    __slots__ = ("_state", "gain", "kind", "url")
37
38    type_letter = "A"
39
40    def __init__(
41        self, system_name: str, user_name: str | None = None,
42    ) -> None:
43        super().__init__(system_name, user_name)
44        self._state: int = AUDIO_STATE_STOPPED
45        self.kind: AudioKind = "source"
46        # Source: URL of a buffer to play. Buffer: URL of the sample file.
47        # Listener: unused. All are opaque strings for the JSON layer.
48        self.url: str | None = None
49        # Source gain 0..1. Ignored for buffer / listener.
50        self.gain: float = 1.0
51
52    @property
53    def state(self) -> int:
54        return self._state
55
56    async def play(self) -> None:
57        """Mark this source as playing; emits ``state_changed``."""
58        if self._state == AUDIO_STATE_PLAYING:
59            return
60        self._state = AUDIO_STATE_PLAYING
61        await self.state_changed.emit(self._state)
62
63    async def stop(self) -> None:
64        """Mark this source as stopped; emits ``state_changed``."""
65        if self._state == AUDIO_STATE_STOPPED:
66            return
67        self._state = AUDIO_STATE_STOPPED
68        await self.state_changed.emit(self._state)

One audio object. Kind selects between the three JMRI audio roles.

Audio(system_name: str, user_name: str | None = None)
40    def __init__(
41        self, system_name: str, user_name: str | None = None,
42    ) -> None:
43        super().__init__(system_name, user_name)
44        self._state: int = AUDIO_STATE_STOPPED
45        self.kind: AudioKind = "source"
46        # Source: URL of a buffer to play. Buffer: URL of the sample file.
47        # Listener: unused. All are opaque strings for the JSON layer.
48        self.url: str | None = None
49        # Source gain 0..1. Ignored for buffer / listener.
50        self.gain: float = 1.0
type_letter = 'A'
kind: Literal['source', 'buffer', 'listener']
url: str | None
gain: float
state: int
52    @property
53    def state(self) -> int:
54        return self._state
async def play(self) -> None:
56    async def play(self) -> None:
57        """Mark this source as playing; emits ``state_changed``."""
58        if self._state == AUDIO_STATE_PLAYING:
59            return
60        self._state = AUDIO_STATE_PLAYING
61        await self.state_changed.emit(self._state)

Mark this source as playing; emits state_changed.

async def stop(self) -> None:
63    async def stop(self) -> None:
64        """Mark this source as stopped; emits ``state_changed``."""
65        if self._state == AUDIO_STATE_STOPPED:
66            return
67        self._state = AUDIO_STATE_STOPPED
68        await self.state_changed.emit(self._state)

Mark this source as stopped; emits state_changed.

71class AudioManager(Manager[Audio]):
72    type_letter = "A"
73
74    def _create(self, system_name: str) -> Audio:
75        return Audio(system_name)

Per-type bean registry. Subclass and override _create().

Attributes set on the class describe the bean type:

  • type_letter — single uppercase letter the bean class owns (e.g. "T" for Turnout). Must be set by subclass.
  • system_prefix — connection prefix (e.g. "L"). Set per-instance because one bean class can have many connection-specific managers.
type_letter = 'A'