def getSerialPorts(cls) -> List[Optional[str]]: """Lists serial port names :raises EnvironmentError: On unsupported or unknown platforms :returns: A list of the serial ports available on the system """ if isWindows(): ports = [port.device for port in comports()] elif isLinux(): # this excludes your current terminal "/dev/tty" ports = glob.glob("/dev/tty[A-Za-z]*") elif isMacOS(): ports = glob.glob("/dev/tty.*") else: raise EnvironmentError("Unsupported platform") valid: List[str] = ports result: List[Optional[str]] = [] if len(valid) > 0: valid.sort() result.append(None) # Add the None option result.extend(valid) return result
def _getSDAudioDevices(cls): # To update the list of devices # Sadly this only works on Windows. Linux hangs, MacOS crashes. if isWindows(): sd._terminate() sd._initialize() devices: sd.DeviceList = sd.query_devices() return devices
def getAudioOutputs(cls) -> Tuple[List[Dict]]: host_apis = list(sd.query_hostapis()) devices: sd.DeviceList = cls._getSDAudioDevices() for host_api_id in range(len(host_apis)): # Linux SDL uses PortAudio, which SoundDevice doesn't find. So mark all as unsable. if (isWindows() and host_apis[host_api_id]["name"] not in WINDOWS_APIS) or (isLinux()): host_apis[host_api_id]["usable"] = False else: host_apis[host_api_id]["usable"] = True host_api_devices = (device for device in devices if device["hostapi"] == host_api_id) outputs: List[Dict] = list(filter(cls._isOutput, host_api_devices)) outputs = sorted(outputs, key=lambda k: k["name"]) host_apis[host_api_id]["output_devices"] = outputs return host_apis
def __init__(self, channel_from_q: List[Queue], server_config: StateManager): self.logger = LoggingManager("FileManager") self.api = MyRadioAPI(self.logger, server_config) process_title = "File Manager" setproctitle(process_title) current_process().name = process_title terminator = Terminator() self.normalisation_mode = server_config.get()["normalisation_mode"] if self.normalisation_mode != "on": self.logger.log.info("Normalisation is disabled.") else: self.logger.log.info("Normalisation is enabled.") self.channel_count = len(channel_from_q) self.channel_received = None self.last_known_show_plan = [[]] * self.channel_count self.next_channel_preload = 0 self.known_channels_preloaded = [False] * self.channel_count self.known_channels_normalised = [False] * self.channel_count self.last_known_item_ids = [[]] * self.channel_count try: while not terminator.terminate: # If all channels have received the delete command, reset for the next one. if ( self.channel_received is None or self.channel_received == [True] * self.channel_count ): self.channel_received = [False] * self.channel_count for channel in range(self.channel_count): try: message = channel_from_q[channel].get_nowait() except Exception: continue try: # source = message.split(":")[0] command = message.split(":", 2)[1] # If we have requested a new show plan, empty the music-tmp directory for the previous show. if command == "GETPLAN": if ( self.channel_received != [ False] * self.channel_count and self.channel_received[channel] is False ): # We've already received a delete trigger on a channel, # let's not delete the folder more than once. # If the channel was already in the process of being deleted, the user has # requested it again, so allow it. self.channel_received[channel] = True continue # Delete the previous show files! # Note: The players load into RAM. If something is playing over the load, # the source file can still be deleted. path: str = resolve_external_file_path( "/music-tmp/") if not os.path.isdir(path): self.logger.log.warning( "Music-tmp folder is missing, not handling." ) continue files = [ f for f in os.listdir(path) if os.path.isfile(os.path.join(path, f)) ] for file in files: if isWindows(): filepath = path + "\\" + file else: filepath = path + "/" + file self.logger.log.info( "Removing file {} on new show load.".format( filepath ) ) try: os.remove(filepath) except Exception: self.logger.log.warning( "Failed to remove, skipping. Likely file is still in use." ) continue self.channel_received[channel] = True self.known_channels_preloaded = [ False] * self.channel_count self.known_channels_normalised = [ False ] * self.channel_count # If we receive a new status message, let's check for files which have not been pre-loaded. if command == "STATUS": extra = message.split(":", 3) if extra[2] != "OKAY": continue status = json.loads(extra[3]) show_plan = status["show_plan"] item_ids = [] for item in show_plan: item_ids += item["timeslotitemid"] # If the new status update has a different order / list of items, # let's update the show plan we know about # This will trigger the chunk below to do the rounds again and preload any new files. if item_ids != self.last_known_item_ids[channel]: self.last_known_item_ids[channel] = item_ids self.last_known_show_plan[channel] = show_plan self.known_channels_preloaded[channel] = False except Exception: self.logger.log.exception( "Failed to handle message {} on channel {}.".format( message, channel ) ) # Let's try preload / normalise some files now we're free of messages. preloaded = self.do_preload() normalised = self.do_normalise() if not preloaded and not normalised: # We didn't do any hard work, let's sleep. sleep(0.2) except Exception as e: self.logger.log.exception( "Received unexpected exception: {}".format(e)) del self.logger
import json import os from helpers.os_environment import isWindows dir_path = os.path.dirname(os.path.realpath(__file__)) parent_path = os.path.dirname(dir_path) in_file = open('build-exe-config.template.json', 'r') config = json.loads(in_file.read()) in_file.close() for option in config["pyinstallerOptions"]: if option["optionDest"] in ["datas", "filenames", "icon_file"]: # If we wanted a relative output directory, this will go missing in abspath on windows. relative_fix = False split = option["value"].split(";") if len(split) > 1 and split[1] == "./": relative_fix = True option["value"] = os.path.abspath(parent_path + option["value"]) if not isWindows(): option["value"] = option["value"].replace(";", ":") elif relative_fix: # Add the windows relative path. option["value"] += "./" out_file = open('build-exe-config.json', 'w') out_file.write(json.dumps(config, indent=2)) out_file.close()
from typing import Any, Dict, List, Optional, Tuple import sounddevice as sd from helpers.os_environment import isLinux, isMacOS, isWindows import os os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "hide" if isLinux(): os.putenv('SDL_AUDIODRIVER', 'pulseaudio') import pygame._sdl2 as sdl2 from pygame import mixer import glob if isWindows(): from serial.tools.list_ports_windows import comports # TODO: https://wiki.libsdl.org/FAQUsingSDL maybe try setting some of these env variables for choosing different host APIs? WINDOWS_APIS = ["Windows DirectSound"] class DeviceManager: @classmethod def _isOutput(cls, device: Dict[str, Any]) -> bool: return device["max_output_channels"] > 0 @classmethod def _isHostAPI(cls, host_api) -> bool: return host_api @classmethod def _getSDAudioDevices(cls): # To update the list of devices