#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Fenrir TTY screen reader # By Chrys, Storm Dragon, and contributors. import shlex import subprocess from fenrirscreenreader.core import debug from fenrirscreenreader.core.soundDriver import sound_driver class driver(sound_driver): """Generic sound driver for Fenrir screen reader. This driver provides sound playback through external command-line tools like sox, aplay, or other audio utilities. It supports both sound file playback and frequency/tone generation. Features: - Configurable external command execution - Sound file playback (WAV, OGG, etc.) - Frequency/tone generation - Process management and cancellation Attributes: proc: Currently running subprocess for sound playback soundFileCommand (str): Command template for playing sound files frequenceCommand (str): Command template for generating frequencies """ def __init__(self): sound_driver.__init__(self) self.proc = None self.soundType = "" self.soundFileCommand = "" self.frequenceCommand = "" def initialize(self, environment): """Initialize the generic sound driver. Loads command templates from configuration for sound file playback and frequency generation. Args: environment: Fenrir environment dictionary with settings """ self.env = environment self.soundFileCommand = self.env["runtime"][ "SettingsManager" ].get_setting("sound", "genericPlayFileCommand") self.frequenceCommand = self.env["runtime"][ "SettingsManager" ].get_setting("sound", "genericFrequencyCommand") if self.soundFileCommand == "": self.soundFileCommand = "play -q -v fenrirVolume fenrirSoundFile" if self.frequenceCommand == "": self.frequenceCommand = ( "play -q -v fenrirVolume -n -c1 synth fenrirDuration sine fenrirFrequence" ) self._initialized = True def play_frequence( self, frequence, duration, adjust_volume=0.0, interrupt=True ): """Play a tone at the specified frequency. Args: frequence (float): Frequency in Hz duration (float): Duration in seconds adjust_volume (float): Volume adjustment interrupt (bool): Whether to interrupt current sound """ if not self._initialized: return if interrupt: self.cancel() popen_frequence_command = shlex.split(self.frequenceCommand) for idx, word in enumerate(popen_frequence_command): word = word.replace( "fenrirVolume", str(self.volume * adjust_volume) ) word = word.replace("fenrirDuration", str(duration)) word = word.replace("fenrirFrequence", str(frequence)) popen_frequence_command[idx] = word self.proc = subprocess.Popen( popen_frequence_command, stdin=None, stdout=None, stderr=None, shell=False, ) self.soundType = "frequence" def play_sound_file(self, file_path, interrupt=True): """Play a sound file. Args: file_path (str): Path to the sound file to play interrupt (bool): Whether to interrupt current sound """ if not self._initialized: return if interrupt: self.cancel() # Validate file path to prevent injection import os if not os.path.isfile(file_path) or ".." in file_path: return popen_sound_file_command = shlex.split(self.soundFileCommand) for idx, word in enumerate(popen_sound_file_command): word = word.replace("fenrirVolume", str(self.volume)) word = word.replace("fenrirSoundFile", shlex.quote(str(file_path))) popen_sound_file_command[idx] = word self.proc = subprocess.Popen(popen_sound_file_command, shell=False) self.soundType = "file" def cancel(self): """Cancel currently playing sound. Terminates the subprocess playing sound and cleans up resources. """ if not self._initialized: return if self.soundType == "": return if self.soundType == "file": self.proc.kill() try: # Wait for process to finish to prevent zombies self.proc.wait(timeout=1.0) except subprocess.TimeoutExpired: pass # Process already terminated except Exception as e: pass # Handle any other wait errors if self.soundType == "frequence": self.proc.kill() try: # Wait for process to finish to prevent zombies self.proc.wait(timeout=1.0) except subprocess.TimeoutExpired: pass # Process already terminated except Exception as e: pass # Handle any other wait errors self.soundType = ""