Work on sound positioning for objects. Enable in sounds in preferences.

This commit is contained in:
Storm Dragon
2026-05-08 07:41:44 -04:00
parent 6f33caade1
commit a5f7c9a8f3
13 changed files with 304 additions and 30 deletions
+69
View File
@@ -16,6 +16,7 @@ speech_stub.speak = mock.Mock()
sys.modules.setdefault("cthulhu.speech", speech_stub)
from cthulhu import settings
from cthulhu.sound_generator import SoundGenerator
from cthulhu.sound_theme_manager import SoundThemeManager
from cthulhu.speech_generator import SpeechGenerator
from cthulhu.script_utilities import Utilities
@@ -146,6 +147,74 @@ class LinkIndicatorPresentationRegressionTests(unittest.TestCase):
self.assertEqual(spoken, "Docs link")
self.assertEqual(icons, [icon])
def test_sound_pan_uses_object_position_inside_active_frame(self):
utility = object.__new__(Utilities)
obj = mock.Mock()
frame = mock.Mock()
icon = SimpleNamespace(path="/tmp/link.wav")
fakeApp = mock.Mock()
fakeApp.settingsManager.getSetting.return_value = True
def get_rect(target):
if target is frame:
return SimpleNamespace(x=0, y=0, width=1000, height=800)
if target is obj:
return SimpleNamespace(x=495, y=0, width=10, height=20)
return SimpleNamespace(x=0, y=0, width=0, height=0)
utility.frameAndDialog = mock.Mock(return_value=[frame, None])
with (
mock.patch("cthulhu.script_utilities.cthulhu.cthulhuApp", fakeApp),
mock.patch("cthulhu.script_utilities.AXComponent.get_rect", side_effect=get_rect),
):
utility.spatializeSoundIcon(icon, obj)
self.assertEqual(icon.pan, 0.0)
def test_sound_pan_hard_pans_only_at_extreme_edges(self):
utility = object.__new__(Utilities)
obj = mock.Mock()
frame = mock.Mock()
fakeApp = mock.Mock()
fakeApp.settingsManager.getSetting.return_value = True
utility.frameAndDialog = mock.Mock(return_value=[frame, None])
with (
mock.patch("cthulhu.script_utilities.cthulhu.cthulhuApp", fakeApp),
mock.patch(
"cthulhu.script_utilities.AXComponent.get_rect",
side_effect=[
SimpleNamespace(x=0, y=0, width=10, height=20),
SimpleNamespace(x=0, y=0, width=1000, height=800),
],
),
):
self.assertEqual(utility.getSoundPanForObject(obj), -1.0)
class SoundGeneratorSpatializationRegressionTests(unittest.TestCase):
def test_generated_icons_and_tones_are_spatialized(self):
generator = object.__new__(SoundGenerator)
icon = SimpleNamespace(path="/tmp/link.wav")
tone = SimpleNamespace(duration=0.1, frequency=440, volume=0.5, wave=0)
generator._script = mock.Mock()
generator._script.utilities.spatializeSoundIcon.side_effect = lambda sound, obj: sound
fakeApp = mock.Mock()
fakeApp.settingsManager.getSetting.return_value = True
obj = mock.Mock()
with mock.patch("cthulhu.sound_generator.cthulhu.cthulhuApp", fakeApp):
result = generator._spatializeSounds([icon, [tone]], obj)
self.assertEqual(result, [icon, [tone]])
generator._script.utilities.spatializeSoundIcon.assert_has_calls(
[mock.call(icon, obj), mock.call(tone, obj)]
)
if __name__ == "__main__":
unittest.main()
@@ -1,11 +1,13 @@
import sys
import unittest
import importlib
from pathlib import Path
from unittest import mock
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
from cthulhu import speech
sys.modules.pop("cthulhu.speech", None)
speech = importlib.import_module("cthulhu.speech")
class PresentationInterruptSoundRegressionTests(unittest.TestCase):
+28 -6
View File
@@ -20,36 +20,48 @@ class _FakePlaybin:
class SoundHelperBackendTests(unittest.TestCase):
def test_create_file_player_uses_playbin_default_sink_for_auto(self):
fakePlaybin = _FakePlaybin()
fakePanorama = object()
with (
mock.patch.object(
sound_helper.Gst.ElementFactory,
"make",
return_value=fakePlaybin,
side_effect=[fakePlaybin, fakePanorama],
) as makeElement,
mock.patch.object(
sound_helper.sound_sink,
"create_audio_sink",
) as createAudioSink,
):
player, sinkName, error = sound_helper._create_file_player("worker-file", settings.SOUND_SINK_AUTO)
player, panorama, sinkName, error = sound_helper._create_file_player(
"worker-file",
settings.SOUND_SINK_AUTO,
)
self.assertIs(player, fakePlaybin)
self.assertIs(panorama, fakePanorama)
self.assertEqual(sinkName, "playbin-default")
self.assertIsNone(error)
createAudioSink.assert_not_called()
makeElement.assert_called_once_with("playbin", "worker-file")
makeElement.assert_has_calls(
[
mock.call("playbin", "worker-file"),
mock.call("audiopanorama", "worker-file-panorama"),
]
)
self.assertNotIn("audio-sink", fakePlaybin.properties)
self.assertIs(fakePlaybin.properties["audio-filter"], fakePanorama)
def test_create_file_player_sets_explicit_sink_when_requested(self):
fakePlaybin = _FakePlaybin()
fakePanorama = object()
fakeSink = object()
with (
mock.patch.object(
sound_helper.Gst.ElementFactory,
"make",
return_value=fakePlaybin,
side_effect=[fakePlaybin, fakePanorama],
) as makeElement,
mock.patch.object(
sound_helper.sound_sink,
@@ -57,14 +69,24 @@ class SoundHelperBackendTests(unittest.TestCase):
return_value=(fakeSink, "pulsesink", None),
) as createAudioSink,
):
player, sinkName, error = sound_helper._create_file_player("worker-file", settings.SOUND_SINK_PULSE)
player, panorama, sinkName, error = sound_helper._create_file_player(
"worker-file",
settings.SOUND_SINK_PULSE,
)
self.assertIs(player, fakePlaybin)
self.assertIs(panorama, fakePanorama)
self.assertEqual(sinkName, "pulsesink")
self.assertIsNone(error)
makeElement.assert_called_once_with("playbin", "worker-file")
makeElement.assert_has_calls(
[
mock.call("playbin", "worker-file"),
mock.call("audiopanorama", "worker-file-panorama"),
]
)
createAudioSink.assert_called_once()
self.assertIs(fakePlaybin.properties["audio-sink"], fakeSink)
self.assertIs(fakePlaybin.properties["audio-filter"], fakePanorama)
if __name__ == "__main__":
@@ -41,6 +41,7 @@ class SoundPreferencesBuilderTests(unittest.TestCase):
self.assertIn("soundsTabLabel", objectIds)
self.assertIn("enableSoundCheckButton", objectIds)
self.assertIn("soundVolumeScale", objectIds)
self.assertIn("spatializeObjectSoundsCheckButton", objectIds)
self.assertIn("progressBarBeepIntervalSpinButton", objectIds)
def test_notebook_tab_positions_keep_sounds_page_and_label_aligned(self):
@@ -90,6 +91,7 @@ class SoundPreferencesControllerTests(unittest.TestCase):
"playSoundForState": True,
"playSoundForPositionInSet": False,
"playSoundForValue": False,
"spatializeObjectSounds": True,
}
widgets = {
"soundSinkCombo": mock.Mock(),
@@ -102,6 +104,7 @@ class SoundPreferencesControllerTests(unittest.TestCase):
"playSoundForStateCheckButton": mock.Mock(),
"playSoundForPositionInSetCheckButton": mock.Mock(),
"playSoundForValueCheckButton": mock.Mock(),
"spatializeObjectSoundsCheckButton": mock.Mock(),
"beepProgressBarUpdatesCheckButton": mock.Mock(),
}
gui.get_widget = widgets.__getitem__
@@ -128,6 +131,7 @@ class SoundPreferencesControllerTests(unittest.TestCase):
widgets["playSoundForStateCheckButton"].set_active.assert_called_once_with(True)
widgets["playSoundForPositionInSetCheckButton"].set_active.assert_called_once_with(False)
widgets["playSoundForValueCheckButton"].set_active.assert_called_once_with(False)
widgets["spatializeObjectSoundsCheckButton"].set_active.assert_called_once_with(True)
widgets["beepProgressBarUpdatesCheckButton"].set_active.assert_called_once_with(True)
widgets["progressBarBeepIntervalSpinButton"].set_value.assert_called_once_with(0)
+25
View File
@@ -127,6 +127,31 @@ class PlayerRecoveryTests(unittest.TestCase):
sendCommand.assert_called_once_with({"action": "stop"}, waitForResponse=False)
def test_icon_pan_is_sent_to_worker(self):
player = sound.Player()
icon = sound.Icon("/tmp", "link.wav")
icon.pan = -0.5
with (
mock.patch.object(icon, "isValid", return_value=True),
mock.patch.object(player, "_sendWorkerCommand", return_value=(True, None)) as sendCommand,
):
player.play(icon)
command = sendCommand.call_args.args[0]
self.assertEqual(command["pan"], -0.5)
def test_tone_pan_is_sent_to_worker(self):
player = sound.Player()
tone = sound.Tone(0.1, 440)
tone.pan = 0.5
with mock.patch.object(player, "_sendWorkerCommand", return_value=(True, None)) as sendCommand:
player.play(tone)
command = sendCommand.call_args.args[0]
self.assertEqual(command["pan"], 0.5)
if __name__ == "__main__":
unittest.main()