Work on sound positioning for objects. Enable in sounds in preferences.
This commit is contained in:
@@ -1359,6 +1359,23 @@
|
||||
<property name="width">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="spatializeObjectSoundsCheckButton">
|
||||
<property name="label" translatable="yes">Use stereo _position for object sounds</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="xalign">0</property>
|
||||
<property name="draw_indicator">True</property>
|
||||
<signal name="toggled" handler="checkButtonToggled" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">6</property>
|
||||
<property name="width">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
|
||||
@@ -3136,6 +3136,9 @@ print(json.dumps(result))
|
||||
self.get_widget("playSoundForValueCheckButton").set_active(
|
||||
prefs.get("playSoundForValue", settings.playSoundForValue)
|
||||
)
|
||||
self.get_widget("spatializeObjectSoundsCheckButton").set_active(
|
||||
prefs.get("spatializeObjectSounds", settings.spatializeObjectSounds)
|
||||
)
|
||||
self.get_widget("beepProgressBarUpdatesCheckButton").set_active(
|
||||
prefs.get("beepProgressBarUpdates", settings.beepProgressBarUpdates)
|
||||
)
|
||||
|
||||
@@ -66,6 +66,7 @@ from . import pronunciation_dict
|
||||
from . import settings
|
||||
from . import settings_manager
|
||||
from . import text_attribute_names
|
||||
from .ax_component import AXComponent
|
||||
from .ax_document_selection import AXDocumentSelection
|
||||
from .ax_object import AXObject
|
||||
from .ax_selection import AXSelection
|
||||
@@ -3259,6 +3260,10 @@ class Utilities:
|
||||
|
||||
icon = manager.getLinkSoundIcon(visited=AXUtilities.is_visited(link))
|
||||
if icon:
|
||||
rect = None
|
||||
if cthulhu.cthulhuApp.settingsManager.getSetting('spatializeObjectSounds'):
|
||||
rect = AXText.get_range_rect(obj, start_index, end_index)
|
||||
self.spatializeSoundIcon(icon, link, rect)
|
||||
icons.append(icon)
|
||||
else:
|
||||
missingIcon = True
|
||||
@@ -3271,6 +3276,65 @@ class Utilities:
|
||||
|
||||
return spoken, icons
|
||||
|
||||
def spatializeSoundIcon(
|
||||
self,
|
||||
icon: Any,
|
||||
obj: Optional[Atspi.Accessible],
|
||||
rect: Optional[Any] = None,
|
||||
) -> Any:
|
||||
"""Attach an optional stereo pan value to an icon based on object position."""
|
||||
|
||||
if not cthulhu.cthulhuApp.settingsManager.getSetting('spatializeObjectSounds'):
|
||||
return icon
|
||||
|
||||
pan = self.getSoundPanForObject(obj, rect)
|
||||
if pan is None:
|
||||
return icon
|
||||
|
||||
try:
|
||||
icon.pan = pan
|
||||
except Exception:
|
||||
pass
|
||||
return icon
|
||||
|
||||
def getSoundPanForObject(
|
||||
self,
|
||||
obj: Optional[Atspi.Accessible],
|
||||
rect: Optional[Any] = None,
|
||||
) -> Optional[float]:
|
||||
"""Return a conservative left/right pan value for obj, or None if unknown."""
|
||||
|
||||
if obj is None and rect is None:
|
||||
return None
|
||||
|
||||
rect = rect or AXComponent.get_rect(obj)
|
||||
width = getattr(rect, "width", 0)
|
||||
if width <= 0:
|
||||
return None
|
||||
|
||||
frame, dialog = self.frameAndDialog(obj)
|
||||
reference = dialog or frame or self.activeWindow()
|
||||
if reference is None:
|
||||
return None
|
||||
|
||||
referenceRect = AXComponent.get_rect(reference)
|
||||
referenceWidth = getattr(referenceRect, "width", 0)
|
||||
if referenceWidth <= 0:
|
||||
return None
|
||||
|
||||
centerX = getattr(rect, "x", 0) + width / 2
|
||||
left = getattr(referenceRect, "x", 0)
|
||||
position = (centerX - left) / referenceWidth
|
||||
position = max(0.0, min(1.0, position))
|
||||
|
||||
edge = 0.05
|
||||
if position <= edge:
|
||||
return -1.0
|
||||
if position >= 1.0 - edge:
|
||||
return 1.0
|
||||
|
||||
return ((position - edge) / (1.0 - (2 * edge)) * 2.0) - 1.0
|
||||
|
||||
def _processMultiCaseString(self, string: Any) -> Any:
|
||||
return re.sub(r'(?<=[a-z])(?=[A-Z])', ' ', string)
|
||||
|
||||
|
||||
@@ -90,6 +90,7 @@ userCustomizableSettings = [
|
||||
"playSoundForState",
|
||||
"playSoundForPositionInSet",
|
||||
"playSoundForValue",
|
||||
"spatializeObjectSounds",
|
||||
"roleSoundPresentation",
|
||||
"soundTheme",
|
||||
"verbalizePunctuationStyle",
|
||||
@@ -353,6 +354,7 @@ playSoundForRole = False
|
||||
playSoundForState = False
|
||||
playSoundForPositionInSet = False
|
||||
playSoundForValue = False
|
||||
spatializeObjectSounds = False
|
||||
roleSoundPresentation = ROLE_SOUND_PRESENTATION_SOUND_AND_SPEECH
|
||||
soundTheme = "default"
|
||||
|
||||
|
||||
@@ -154,6 +154,7 @@ class Player:
|
||||
"path": icon.path,
|
||||
"volume": self._get_configured_volume(),
|
||||
"interrupt": interrupt,
|
||||
"pan": getattr(icon, "pan", 0.0),
|
||||
},
|
||||
waitForResponse=False,
|
||||
)
|
||||
@@ -176,6 +177,7 @@ class Player:
|
||||
"path": icon.path,
|
||||
"volume": self._get_configured_volume(),
|
||||
"interrupt": interrupt,
|
||||
"pan": getattr(icon, "pan", 0.0),
|
||||
},
|
||||
waitForResponse=True,
|
||||
timeout=timeout,
|
||||
@@ -215,6 +217,7 @@ class Player:
|
||||
"frequency": tone.frequency,
|
||||
"volume": tone.volume,
|
||||
"wave": tone.wave,
|
||||
"pan": getattr(tone, "pan", 0.0),
|
||||
"interrupt": interrupt,
|
||||
}
|
||||
|
||||
@@ -582,6 +585,7 @@ def playIconSafely(icon: Icon, timeoutSeconds: int = 10) -> Tuple[bool, Optional
|
||||
"path": icon.path,
|
||||
"volume": Player._get_configured_volume(),
|
||||
"interrupt": True,
|
||||
"pan": getattr(icon, "pan", 0.0),
|
||||
},
|
||||
waitForResponse=True,
|
||||
timeout=max(0.1, float(timeoutSeconds)),
|
||||
|
||||
@@ -52,8 +52,9 @@ METHOD_PREFIX = "_generate"
|
||||
class Icon:
|
||||
"""Sound file representing a particular aspect of an object."""
|
||||
|
||||
def __init__(self, location, filename):
|
||||
def __init__(self, location, filename, pan=0.0):
|
||||
self.path = os.path.join(location, filename)
|
||||
self.pan = max(-1.0, min(1.0, float(pan)))
|
||||
|
||||
def __str__(self):
|
||||
return f'Icon(path: {self.path}, isValid: {self.isValid()})'
|
||||
@@ -83,6 +84,7 @@ class Tone:
|
||||
self.frequency = min(max(0, frequency), 20000)
|
||||
self.volume = cthulhu.cthulhuApp.settingsManager.getSetting('soundVolume') * volumeMultiplier
|
||||
self.wave = wave
|
||||
self.pan = 0.0
|
||||
|
||||
def __str__(self):
|
||||
return 'Tone(duration: %s, frequency: %s, volume: %s, wave: %s)' \
|
||||
@@ -105,7 +107,24 @@ class SoundGenerator(generator.Generator):
|
||||
def generateSound(self, obj, **args):
|
||||
"""Returns an array of sounds for the complete presentation of obj."""
|
||||
|
||||
return self.generate(obj, **args)
|
||||
result = self.generate(obj, **args)
|
||||
return self._spatializeSounds(result, obj)
|
||||
|
||||
def _spatializeSounds(self, sounds, obj):
|
||||
if not cthulhu.cthulhuApp.settingsManager.getSetting('spatializeObjectSounds'):
|
||||
return sounds
|
||||
|
||||
spatialize = getattr(self._script.utilities, "spatializeSoundIcon", None)
|
||||
if spatialize is None:
|
||||
return sounds
|
||||
|
||||
result = []
|
||||
for sound in sounds or []:
|
||||
if isinstance(sound, list):
|
||||
result.append(self._spatializeSounds(sound, obj))
|
||||
else:
|
||||
result.append(spatialize(sound, obj))
|
||||
return result
|
||||
|
||||
#####################################################################
|
||||
# #
|
||||
|
||||
+35
-13
@@ -40,6 +40,13 @@ def _clamp_volume(volume: Any) -> float:
|
||||
return 1.0
|
||||
|
||||
|
||||
def _clamp_pan(pan: Any) -> float:
|
||||
try:
|
||||
return max(-1.0, min(1.0, float(pan)))
|
||||
except Exception:
|
||||
return 0.0
|
||||
|
||||
|
||||
def _write_json_line(payload: dict[str, Any]) -> None:
|
||||
sys.stdout.write(json.dumps(payload) + "\n")
|
||||
sys.stdout.flush()
|
||||
@@ -52,24 +59,28 @@ def _report_recovery_required(message: str) -> None:
|
||||
def _create_file_player(
|
||||
playerId: str,
|
||||
soundSink: Optional[str] = None,
|
||||
) -> tuple[Optional[Any], Optional[str], Optional[str]]:
|
||||
) -> tuple[Optional[Any], Optional[Any], Optional[str], Optional[str]]:
|
||||
player = Gst.ElementFactory.make("playbin", playerId)
|
||||
if player is None:
|
||||
return None, None, "Failed to create playbin for file playback"
|
||||
return None, None, None, "Failed to create playbin for file playback"
|
||||
|
||||
panorama = Gst.ElementFactory.make("audiopanorama", f"{playerId}-panorama")
|
||||
if panorama is not None:
|
||||
player.set_property("audio-filter", panorama)
|
||||
|
||||
configuredSink = sound_sink.normalize_sound_sink_choice(soundSink)
|
||||
if configuredSink == settings.SOUND_SINK_AUTO:
|
||||
return player, "playbin-default", None
|
||||
return player, panorama, "playbin-default", None
|
||||
|
||||
audioSink, sinkName, sinkError = sound_sink.create_audio_sink(
|
||||
f"{playerId}-output",
|
||||
configuredSink,
|
||||
)
|
||||
if audioSink is None:
|
||||
return None, sinkName, sinkError or f"Failed to create audio sink for {configuredSink}"
|
||||
return None, panorama, sinkName, sinkError or f"Failed to create audio sink for {configuredSink}"
|
||||
|
||||
player.set_property("audio-sink", audioSink)
|
||||
return player, sinkName or configuredSink, None
|
||||
return player, panorama, sinkName or configuredSink, None
|
||||
|
||||
|
||||
class SoundWorker:
|
||||
@@ -85,7 +96,7 @@ class SoundWorker:
|
||||
self._currentCommand: Optional[dict[str, Any]] = None
|
||||
self._toneTimeoutId = 0
|
||||
|
||||
self._filePlayer, fileSinkName, fileSinkError = _create_file_player(
|
||||
self._filePlayer, self._filePanorama, fileSinkName, fileSinkError = _create_file_player(
|
||||
"cthulhu-sound-worker-file",
|
||||
soundSink,
|
||||
)
|
||||
@@ -104,6 +115,7 @@ class SoundWorker:
|
||||
|
||||
self._toneSource = Gst.ElementFactory.make('audiotestsrc', 'cthulhu-sound-worker-source')
|
||||
self._toneVolume = Gst.ElementFactory.make('volume', 'cthulhu-sound-worker-volume')
|
||||
self._tonePanorama = Gst.ElementFactory.make('audiopanorama', 'cthulhu-sound-worker-panorama')
|
||||
toneSink, toneSinkName, toneSinkError = sound_sink.create_audio_sink(
|
||||
'cthulhu-sound-worker-tone-output',
|
||||
soundSink
|
||||
@@ -115,14 +127,18 @@ class SoundWorker:
|
||||
self._tonePipeline.add(self._toneSource)
|
||||
if self._toneVolume is not None:
|
||||
self._tonePipeline.add(self._toneVolume)
|
||||
if self._tonePanorama is not None:
|
||||
self._tonePipeline.add(self._tonePanorama)
|
||||
self._tonePipeline.add(self._toneSink)
|
||||
toneElements = [self._toneSource]
|
||||
if self._toneVolume is not None:
|
||||
if not self._toneSource.link(self._toneVolume):
|
||||
raise RuntimeError("Failed to link tone source to volume")
|
||||
if not self._toneVolume.link(self._toneSink):
|
||||
raise RuntimeError("Failed to link tone volume to sink")
|
||||
elif not self._toneSource.link(self._toneSink):
|
||||
raise RuntimeError("Failed to link tone source to sink")
|
||||
toneElements.append(self._toneVolume)
|
||||
if self._tonePanorama is not None:
|
||||
toneElements.append(self._tonePanorama)
|
||||
toneElements.append(self._toneSink)
|
||||
for source, target in zip(toneElements, toneElements[1:]):
|
||||
if not source.link(target):
|
||||
raise RuntimeError("Failed to link tone playback pipeline")
|
||||
|
||||
toneBus = self._tonePipeline.get_bus()
|
||||
if toneBus is None:
|
||||
@@ -238,6 +254,8 @@ class SoundWorker:
|
||||
self._currentCommand = command
|
||||
self._filePlayer.set_property("uri", soundPath.resolve().as_uri())
|
||||
self._filePlayer.set_property("volume", _clamp_volume(command.get("volume", 1.0)))
|
||||
if self._filePanorama is not None:
|
||||
self._filePanorama.set_property("panorama", _clamp_pan(command.get("pan", 0.0)))
|
||||
stateChange = self._filePlayer.set_state(Gst.State.PLAYING)
|
||||
if stateChange == Gst.StateChangeReturn.FAILURE:
|
||||
self._currentCommand = None
|
||||
@@ -266,6 +284,8 @@ class SoundWorker:
|
||||
self._toneSource.set_property('volume', 1.0)
|
||||
else:
|
||||
self._toneSource.set_property('volume', _clamp_volume(command.get("volume", 1.0)))
|
||||
if self._tonePanorama is not None:
|
||||
self._tonePanorama.set_property("panorama", _clamp_pan(command.get("pan", 0.0)))
|
||||
self._toneSource.set_property('freq', frequency)
|
||||
self._toneSource.set_property('wave', wave)
|
||||
|
||||
@@ -377,7 +397,7 @@ def play_file_once(
|
||||
print("GStreamer is not available", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
player, sinkName, sinkError = _create_file_player(
|
||||
player, panorama, sinkName, sinkError = _create_file_player(
|
||||
"cthulhu-sound-helper-once",
|
||||
soundSink,
|
||||
)
|
||||
@@ -387,6 +407,8 @@ def play_file_once(
|
||||
|
||||
player.set_property("uri", pathlib.Path(soundPath).resolve().as_uri())
|
||||
player.set_property("volume", _clamp_volume(volume))
|
||||
if panorama is not None:
|
||||
panorama.set_property("panorama", 0.0)
|
||||
player.set_state(Gst.State.PLAYING)
|
||||
|
||||
bus = player.get_bus()
|
||||
|
||||
@@ -122,11 +122,30 @@ class SpeechGenerator(generator.Generator):
|
||||
"""Return the themed sound icon for obj's role, if any."""
|
||||
|
||||
if role == Atspi.Role.LINK or AXUtilities.is_link(obj):
|
||||
return sound_theme_manager.getManager().getLinkSoundIcon(
|
||||
icon = sound_theme_manager.getManager().getLinkSoundIcon(
|
||||
visited=AXUtilities.is_visited(obj)
|
||||
)
|
||||
return self._spatializeRoleSoundIcon(icon, obj)
|
||||
|
||||
return sound_theme_manager.getManager().getRoleSoundIcon(role)
|
||||
icon = sound_theme_manager.getManager().getRoleSoundIcon(role)
|
||||
return self._spatializeRoleSoundIcon(icon, obj)
|
||||
|
||||
def _spatializeRoleSoundIcon(self, icon, obj):
|
||||
"""Spatialize icon if the experimental object sound setting is enabled."""
|
||||
|
||||
try:
|
||||
enabled = cthulhu.cthulhuApp.settingsManager.getSetting('spatializeObjectSounds')
|
||||
except Exception:
|
||||
enabled = False
|
||||
|
||||
if not enabled:
|
||||
return icon
|
||||
|
||||
spatialize = getattr(self._script.utilities, "spatializeSoundIcon", None)
|
||||
if spatialize is None:
|
||||
return icon
|
||||
|
||||
return spatialize(icon, obj)
|
||||
|
||||
def _addGlobals(self, globalsDict):
|
||||
"""Other things to make available from the formatting string.
|
||||
@@ -827,7 +846,7 @@ class SpeechGenerator(generator.Generator):
|
||||
# #
|
||||
#####################################################################
|
||||
|
||||
def _applyStateSound(self, result, role, stateKey):
|
||||
def _applyStateSound(self, result, role, stateKey, obj=None):
|
||||
if not result:
|
||||
return result
|
||||
|
||||
@@ -841,6 +860,7 @@ class SpeechGenerator(generator.Generator):
|
||||
icon = sound_theme_manager.getManager().getRoleStateSoundIcon(role, stateKey)
|
||||
if not icon:
|
||||
return result
|
||||
icon = self._spatializeRoleSoundIcon(icon, obj)
|
||||
|
||||
if roleSoundPresentation == settings.ROLE_SOUND_PRESENTATION_SOUND_ONLY:
|
||||
return [icon]
|
||||
@@ -868,7 +888,7 @@ class SpeechGenerator(generator.Generator):
|
||||
stateKey = "checked"
|
||||
else:
|
||||
stateKey = "unchecked"
|
||||
result = self._applyStateSound(result, role, stateKey)
|
||||
result = self._applyStateSound(result, role, stateKey, obj)
|
||||
return result
|
||||
|
||||
def _generateExpandableState(self, obj, **args):
|
||||
@@ -909,7 +929,8 @@ class SpeechGenerator(generator.Generator):
|
||||
result = self._applyStateSound(
|
||||
result,
|
||||
Atspi.Role.CHECK_MENU_ITEM,
|
||||
"checked"
|
||||
"checked",
|
||||
obj,
|
||||
)
|
||||
return result
|
||||
|
||||
@@ -941,7 +962,7 @@ class SpeechGenerator(generator.Generator):
|
||||
result.extend(self.voice(STATE, obj=obj, **args))
|
||||
stateKey = "checked" if AXUtilities.is_checked(obj) else "unchecked"
|
||||
role = args.get('role', AXObject.get_role(obj))
|
||||
result = self._applyStateSound(result, role, stateKey)
|
||||
result = self._applyStateSound(result, role, stateKey, obj)
|
||||
return result
|
||||
|
||||
def _generateSwitchState(self, obj, **args):
|
||||
@@ -955,7 +976,7 @@ class SpeechGenerator(generator.Generator):
|
||||
stateKey = "checked" if (AXUtilities.is_checked(obj) or AXUtilities.is_pressed(obj)) \
|
||||
else "unchecked"
|
||||
role = args.get('role', AXObject.get_role(obj))
|
||||
result = self._applyStateSound(result, role, stateKey)
|
||||
result = self._applyStateSound(result, role, stateKey, obj)
|
||||
return result
|
||||
|
||||
def _generateToggleState(self, obj, **args):
|
||||
@@ -973,7 +994,7 @@ class SpeechGenerator(generator.Generator):
|
||||
stateKey = "checked" if (AXUtilities.is_checked(obj) or AXUtilities.is_pressed(obj)) \
|
||||
else "unchecked"
|
||||
role = args.get('role', AXObject.get_role(obj))
|
||||
result = self._applyStateSound(result, role, stateKey)
|
||||
result = self._applyStateSound(result, role, stateKey, obj)
|
||||
return result
|
||||
|
||||
#####################################################################
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user