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)
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.
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
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.
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.
Inherited Members
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.