spacr.qt.sound¶
Play spaCR’s sounds: off by default, rendered off the GUI thread, or silent.
The sounds themselves are synthesized by spacr.qt.sound_synth; this
module decides when one plays and hands it to the operating system through
Qt Multimedia’s QSoundEffect.
Three rules shape everything here, and each is a measurement rather than a preference:
Nothing exists while sound is off. The master switch defaults to off, and until somebody turns it on
apply_sound_preferences()returns before constructing anything: no thread, no event filter, no Qt Multimedia import. An application-wide event filter costs every event in the process (measured at 0.93 us per event per filter), so the filter is installed only while a click or hover sound is actually wanted, and removed the moment neither is.Qt Multimedia is touched only on the GUI thread.
_SoundPlayerownsQMediaDevicesand everyQSoundEffectand lives where the application’s event loop does; the dedicatedQThreadkeeps the part that is genuinely slow, which is synthesizing a sound set (_SoundRenderer, numpy and file writes, nothing from Qt Multimedia). Connecting to the sound server is slow – 354 ms measured on a PipeWire workstation – so it is paid ONCE, on an idle timer after sound is switched on (SoundEngine._warm()), and logged rather than felt. Playing an already loaded sound isplay(), which returns at once: 0.007-0.014 ms on the GUI thread against the real stack.This rule is written in a crash. Building the effects on the audio thread instead does move the connection off the GUI thread – but Qt Multimedia’s device handling belongs to the thread that owns the event loop, so with the FFmpeg backend it enables socket notifiers from the wrong thread. Reopening Preferences with sound on then segfaults, or wedges the interface with an empty Preferences frame until it is force quit.
A missing sound stack is silence. No device, no server, no Qt Multimedia, a file that will not load: each ends in a debug log line and no sound. Nothing here raises into a caller and nothing opens a dialog.
The hover sound is the risky one, so it is debounced twice: the pointer must
rest on an enabled control for HOVER_SETTLE_MS before anything is
considered, and a hover sound never follows another within
HOVER_COOLDOWN_S or a click within HOVER_AFTER_CLICK_S.
Classes¶
Application-wide filter that hears presses and hovers on controls. |
|
Decides when a sound plays, and plays it on the thread it belongs to. |
|
What the user has asked to hear, read once and handed around whole. |
Functions¶
|
Play the run-finished or run-failed sound for a run that ended. |
|
Bring sound in line with the stored preferences. |
|
Map a slider fraction to a linear gain that sounds evenly spaced. |
|
Play one sound for a Preview button, starting the engine if needed. |
|
Read every sound preference into one |
|
Stop the process's engine, if there is one, and forget it. |
|
The running engine, or |
|
End any preview and return to the stored settings. |
|
How long a WAV is, from its header alone. |
Module Contents¶
- class spacr.qt.sound.InputSoundFilter(engine: SoundEngine)[source]¶
Bases:
PySide6.QtCore.QObjectApplication-wide filter that hears presses and hovers on controls.
Installed only while a click or hover sound is wanted (see
SoundEngine.apply()). The body is two liveness checks and at most four comparisons for an event it does not want.- Parameters:
engine – the
SoundEngineto tell.
Remember the engine and the kinds of control that make sounds.
- eventFilter(watched, event) bool[source]¶
Tell the engine about presses and hovers; never consume anything.
A control carrying
SILENT_PRESS_PROPERTYis pressed without a click sound. The property is read only for a left press on something pressable, so it costs nothing on the events the filter already ignores.- Parameters:
watched – the object the event is for.
event – the event.
- Returns:
always False, so every event continues as it would have.
- class spacr.qt.sound.SoundEngine(parent: PySide6.QtCore.QObject | None = None, *, threaded: bool = True, effect_factory: Callable | None = None, cache_root: pathlib.Path | None = None)[source]¶
Bases:
PySide6.QtCore.QObjectDecides when a sound plays, and plays it on the thread it belongs to.
Two halves, and which thread each lives on is what keeps sound safe.
_SoundPlayerholds every Qt Multimedia object and lives here, on the GUI thread, because that is the thread that owns the event loop Qt Multimedia’s device handling attaches to._SoundRendererlives on a dedicatedQThreadand synthesizes sound files, which is the only part that is slow enough to need one.- Parameters:
parent – owning object, normally the application.
threaded – run the renderer on its own
QThread.Falseruns it inline, emitting the same calls in the same order, so a test can drive the engine synchronously.effect_factory – see
_SoundPlayer.cache_root – see
_SoundRenderer.
Start the audio thread (when threaded) and wire the requests.
- apply(settings: SoundSettings) None[source]¶
Follow
settings: filter, loaded sounds and music bed.Switching the master off removes the filter and releases every effect, so a disabled engine holds no audio device. The thread itself stays, idle, until the application quits.
Switching it ON also schedules the one device connection, on a timer rather than now: see
_warm().- Parameters:
settings – what to follow from now on.
- audio_thread() PySide6.QtCore.QThread | None[source]¶
The thread sounds are rendered on, or
Nonewhen unthreaded.
- play(event: str) bool[source]¶
Play
event’s sound if the settings want it.- Parameters:
event –
"click","hover","run_finished"or"run_failed".- Returns:
True when a sound was requested.
- preview(event: str, theme_key: str, volume: float, music: str = '') bool[source]¶
Play
eventonce because the user pressed its Preview button.Plays whatever the stored switches say: the dialog enables Preview only while its own master switch is on, and the press is the ask.
- Parameters:
event – any event, the music bed included.
theme_key – the sound set chosen in the dialog.
volume – the dialog’s volume slider as a fraction.
music – the music file named on the page, unsaved. A Preview has to play what the page says and not what the store says, or it is a preview of something else.
- Returns:
True when a sound was requested.
- shutdown(timeout_ms: int = SHUTDOWN_WAIT_MS) bool[source]¶
Stop everything and end the audio thread. Idempotent.
Connected to
aboutToQuit. A render in progress is abandoned at its next file boundary; a thread that still will not stop in time is parked byspacr.qt.bridge.drain_thread()rather than terminated.- Parameters:
timeout_ms – how long to wait for the thread.
- Returns:
True when the thread has stopped.
- property closed: bool[source]¶
Whether
shutdown()has run.
- property settings: SoundSettings[source]¶
The settings this engine is following.
- class spacr.qt.sound.SoundSettings[source]¶
What the user has asked to hear, read once and handed around whole.
- Parameters:
enabled – the master switch.
volume – master volume as a slider fraction, 0 to 1.
theme – key of the sound set in
spacr.qt.sound_synth.SOUND_THEMES.click – play a sound when a control is pressed.
hover – play a sound when the pointer rests on a control.
run_finished – play a sound when a run finishes successfully.
run_failed – play a sound when a run stops with an error.
bed – play the looping music bed. Already False here when the performance level rests it; see
read_sound_settings().music – a WAV of the user’s own to play as the bed instead of the synthesized one. Empty means spaCR’s own. It is also what the Resonance backdrop is driven by, because the backdrop follows WHATEVER IS PLAYING and there is only ever one thing.
- spacr.qt.sound.announce_run_end(status: str) bool[source]¶
Play the run-finished or run-failed sound for a run that ended.
A run the user stopped is silent: they already know.
- Parameters:
status –
"success","failed"or"cancelled".- Returns:
True when a sound was requested.
- spacr.qt.sound.apply_sound_preferences(app=None, settings: SoundSettings | None = None) SoundEngine | None[source]¶
Bring sound in line with the stored preferences.
Called from
spacr.qt.preferences.apply_preferences_to_app(), at launch and after every Save. While sound has never been switched on in this process it reads one setting and returns: nothing is imported or built for a user who has not asked for sound.- Parameters:
app – the application; the running instance when omitted.
settings – settings to follow instead of the stored ones.
- Returns:
the engine, or
Nonewhen there is none.
- spacr.qt.sound.perceived_gain(fraction: float) float[source]¶
Map a slider fraction to a linear gain that sounds evenly spaced.
Loudness is heard roughly logarithmically, so a linear slider would do all its audible work in its bottom quarter. The square is the usual cheap fit.
- Parameters:
fraction – slider position, 0 to 1; clamped.
- Returns:
gain, 0 to 1.
- spacr.qt.sound.preview_sound(event: str, theme_key: str, volume: float, app=None, music: str = '') bool[source]¶
Play one sound for a Preview button, starting the engine if needed.
- Parameters:
event – any event, the music bed included.
theme_key – the sound set chosen in the dialog.
volume – the dialog’s volume slider as a fraction.
app – the application; the running instance when omitted.
music – the music file named on the page, unsaved.
- Returns:
True when a sound was requested.
- spacr.qt.sound.read_sound_settings() SoundSettings[source]¶
Read every sound preference into one
SoundSettings.The music bed is reported off at the performance levels that rest it, whatever its own switch says, so nothing downstream has to know those levels exist.
- Returns:
the settings as stored now.
- spacr.qt.sound.shutdown_sound(timeout_ms: int = SHUTDOWN_WAIT_MS) bool[source]¶
Stop the process’s engine, if there is one, and forget it.
- Parameters:
timeout_ms – how long to wait for the audio thread.
- Returns:
True when nothing is left running.
- spacr.qt.sound.sound_engine() SoundEngine | None[source]¶
The running engine, or
Nonewhen sound was never switched on.
- spacr.qt.sound.stop_sound_preview() None[source]¶
End any preview and return to the stored settings.
- spacr.qt.sound.wav_seconds(path) float[source]¶
How long a WAV is, from its header alone.
The Resonance backdrop needs the loop’s length to work out where in it the playback is, and the header is the cheapest true answer – no samples are read. A file that cannot be opened is reported as zero seconds, which reads downstream as “nothing is playing”.
- Parameters:
path – the file.
- Returns:
seconds, or 0.0.