1 new commit in StationPlaylist: https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/38e040d84991/ Changeset: 38e040d84991 Branch: splengine User: josephsl Date: 2014-11-16 23:39:10+00:00 Summary: SPL Engine (4.0-dev/5.0-dev): Complete rewrite of SAM and SPL Encoder Support. As the encoders are run as DLL's under SPL Engine (splengine.exe), moved encoder support routines from global plugin to the new app module. As this is a highly experimental feature, this may or may not make it into add-on 4.0 (it'll befinitely be included next year in 5.0-dev). Affected #: 2 files diff --git a/addon/appModules/splengine.py b/addon/appModules/splengine.py new file mode 100755 index 0000000..8f3f582 --- /dev/null +++ b/addon/appModules/splengine.py @@ -0,0 +1,287 @@ +# Station Playlist Encoder Engine +# Author: Joseph Lee +# Copyright 2013-2014, released under GPL. + +import threading +import os +import time +from configobj import ConfigObj # Configuration management; configspec will be used to store app module and global plugin settings in one ini file. +import appModuleHandler +import api +import ui +import speech +import braille +import globalVars +import review +import textInfos +import winUser +import tones +import gui +import wx +import addonHandler +addonHandler.initTranslation() + +# SPL Studio uses WM messages to send and receive data, similar to Winamp (see NVDA sources/appModules/winamp.py for more information). +user32 = winUser.user32 # user32.dll. +SPLWin = 0 # A handle to studio window. +SPLMSG = winUser.WM_USER + +# Needed in encoder connection support +SPLPlay = 12 + +# Needed in SAM and SPL Encoder support: +SAMFocusToStudio = {} # A dictionary to record whether to switch to SPL Studio for this encoder. +SPLFocusToStudio = {} +SAMPlayAfterConnecting = {} +SPLPlayAfterConnecting = {} +SAMStreamLabels= {} # A dictionary to store custom labels for each stream. +SPLStreamLabels= {} # Same as above but optimized for SPL encoders (Studio 5.00 and later). + +# Stream labels +labels = ConfigObj(os.path.join(globalVars.appArgs.configPath, "splStreamLabels.ini")) +try: + SAMStreamLabels = dict(labels["SAMEncoders"]) +except KeyError: + SAMStreamLabels = {} +try: + SPLStreamLabels = dict(labels["SPLEncoders"]) +except KeyError: + SPLStreamLabels = {} + + +# Try to see if SPL foreground object can be fetched. This is used for switching to SPL Studio window from anywhere and to switch to Studio window from SAM encoder window. + +def fetchSPLForegroundWindow(): + # Turns out NVDA core does have a method to fetch desktop objects, so use this to find SPL window from among its children. + dt = api.getDesktopObject() + for fg in dt.children: + if "splstudio" in fg.appModule.appModuleName: return fg + return None + + +class AppModule(appModuleHandler.AppModule): + + # Translators: Script category for Station Playlist commands in input gestures dialog. + scriptCategory = _("Station Playlist Studio") + + + def terminate(self): + global labels, SAMStreamLabels, SPLStreamLabels + labels["SAMEncoders"] = SAMStreamLabels + labels["SPLEncoders"] = SPLStreamLabels + labels.write() + + # Announce stream labels + def event_gainFocus(self, obj, nextHandler): + if self.connecting: return + encoderType = self.isEncoderWindow(obj) + streamLabel = None + if encoderType == "SAM": + try: + streamLabel = SAMStreamLabels[obj.name] + except KeyError: + streamLabel = None + elif encoderType == "SPL": + try: + streamLabel = SPLStreamLabels[str(obj.parent.children.index(obj)+1)] + except KeyError: + streamLabel = None + # Speak the stream label if it exists. + if streamLabel is not None: + speech.speakMessage(streamLabel) + if encoderType == "SAM": brailleStreamLabel = str(obj.name) + ": " + streamLabel + elif encoderType == "SPL": + brailleStreamLabel = str(obj.parent.children.index(obj)+1) + ": " + streamLabel + braille.handler.message(brailleStreamLabel) + nextHandler() + + + # Few things to check + focusToStudio = False + playAfterConnecting = False + + def isEncoderWindow(self, obj): + fg = api.getForegroundObject() + if obj.windowClassName == "TListView" and fg.windowClassName == "TfoSCEncoders": + return "SAM" + elif obj.windowClassName == "SysListView32" and "splengine" in obj.appModule.appName: + return "SPL" + return None + + # Routines for each encoder + # Mostly connection monitoring + + def connect_sam(self, encoderWindow, gesture): + gesture.send() + # Translators: Presented when SAM Encoder is trying to connect to a streaming server. + ui.message(_("Connecting...")) + # Oi, status thread, can you keep an eye on the connection status for me? + statusThread = threading.Thread(target=self.reportConnectionStatus_sam, args=(encoderWindow,)) + statusThread.name = "Connection Status Reporter" + statusThread.start() + + def reportConnectionStatus_sam(self, encoderWindow): + # Keep an eye on the stream's description field until connected or error occurs. + # In order to not block NVDA commands, this will be done using a different thread. + SPLWin = user32.FindWindowA("SPLStudio", None) + toneCounter = 0 + while True: + time.sleep(0.001) + toneCounter+=1 + if toneCounter%250 == 0: tones.beep(500, 50) # Play status tones every second. + #info = review.getScreenPosition(self)[0] + #info.expand(textInfos.UNIT_LINE) + #if "Error" in info.text: + if "Error" in encoderWindow.description: + # Announce the description of the error. + ui.message(encoderWindow.description[encoderWindow.description.find("Status")+8:]) + break + #elif "Encoding" in info.text or "Encoded" in info.text: + elif "Encoding" in encoderWindow.description or "Encoded" in encoderWindow.description: + # We're on air, so exit. + if self.focusToStudio: + fetchSPLForegroundWindow().setFocus() + tones.beep(1000, 150) + if self.playAfterConnecting: + winUser.sendMessage(SPLWin, SPLMSG, 0, SPLPlay) + break + + # Only needed in SPL Encoder to prevent focus announcement. + connecting = False + + def connect_spl(self, encoderWindow): + # Same as SAM's connection routine, but this time, keep an eye on self.name and a different connection flag. + self.connecting = True + connectButton = api.getForegroundObject().children[2] + if connectButton.name == "Disconnect": return + ui.message(_("Connecting...")) + # Juggle the focus around. + connectButton.doAction() + encoderWindow.setFocus() + # Same as SAM encoders. + statusThread = threading.Thread(target=self.reportConnectionStatus_spl, args=(encoderWindow,)) + statusThread.name = "Connection Status Reporter" + statusThread.start() + + def reportConnectionStatus_spl(self, encoderWindow): + # Same routine as SAM encoder: use a thread to prevent blocking NVDA commands. + SPLWin = user32.FindWindowA("SPLStudio", None) + attempt = 0 + while True: + time.sleep(0.001) + attempt += 1 + if attempt%250 == 0: tones.beep(500, 50) + #info = review.getScreenPosition(self)[0] + #info.expand(textInfos.UNIT_LINE) + #if not info.text.endswith("Disconnected"): ui.message(info.text) + #if info.text.endswith("Connected"): + if "Unable to connect" in encoderWindow.name: + break + if encoderWindow.children[1].name == "Connected": + # We're on air, so exit. + if self.focusToStudio: + fetchSPLForegroundWindow().setFocus() + tones.beep(1000, 150) + if self.playAfterConnecting: + winUser.sendMessage(SPLWin, SPLMSG, 0, SPLPlay) + break + if encoderWindow.children[1].name != "Connected": + self.connecting = False + ui.message(encoderWindow.children[1].name) + + # Encoder scripts + + def script_connect(self, gesture): + focus = api.getFocusObject() + encoder = self.isEncoderWindow(focus) + if encoder is None: + return + elif encoder == "SAM": + self.connect_sam(focus, gesture) + else: + self.connect_spl(focus) + + def script_disconnect(self, gesture): + gesture.send() + if self.isEncoderWindow(api.getFocusObject()) == "SAM": + # Translators: Presented when SAM Encoder is disconnecting from a streaming server. + ui.message(_("Disconnecting...")) + + def script_toggleFocusToStudio(self, gesture): + if not self.focusToStudio: + self.focusToStudio = True + #elf.encoderType == "SAM": + #SAMFocusToStudio[self.name] = True + #elif self.encoderType == "SPL": + #SPLFocusToStudio[str(self.IAccessibleChildID)] = True + # Translators: Presented when toggling the setting to switch to Studio when connected to a streaming server. + ui.message(_("Switch to Studio after connecting")) + else: + self.focusToStudio = False + # Translators: Presented when toggling the setting to switch to Studio when connected to a streaming server. + ui.message(_("Do not switch to Studio after connecting")) + # Translators: Input help mode message in SAM Encoder window. + script_toggleFocusToStudio.__doc__=_("Toggles whether NVDA will switch to Studio when connected to a streaming server.") + + def script_togglePlay(self, gesture): + if not self.playAfterConnecting: + self.playAfterConnecting = True + #if self.encoderType == "SAM": + #SAMPlayAfterConnecting[self.name] = True + #elif self.encoderType == "SPL": + #SPLPlayAfterConnecting[str(self.IAccessibleChildID)] = True + # Translators: Presented when toggling the setting to play selected song when connected to a streaming server. + ui.message(_("Play first track after connecting")) + else: + self.playAfterConnecting = False + # Translators: Presented when toggling the setting to switch to Studio when connected to a streaming server. + ui.message(_("Do not play first track after connecting")) + # Translators: Input help mode message in SAM Encoder window. + script_togglePlay.__doc__=_("Toggles whether Studio will play the first song when connected to a streaming server.") + + def script_streamLabeler(self, gesture): + encoder = api.getFocusObject() + encoderType = self.isEncoderWindow(encoder) + if not encoderType: + return + else: + curStreamLabel = titleText = "" + if encoderType == "SAM": + try: + curStreamLabel = SAMStreamLabels[encoder.name] + except KeyError: + curStreamLabel = "" + titleText = encoder.name + elif encoderType == "SPL": + childPos = str(encoder.parent.children.index(encoder)+1) + if childPos in SPLStreamLabels: + curStreamLabel = SPLStreamLabels[childPos] + titleText = encoder.firstChild.name + # Translators: The title of the stream labeler dialog (example: stream labeler for 1). + streamTitle = _("Stream labeler for {streamEntry}").format(streamEntry = titleText) + # Translators: The text of the stream labeler dialog. + streamText = _("Enter the label for this stream") + dlg = wx.TextEntryDialog(gui.mainFrame, + streamText, streamTitle, defaultValue=curStreamLabel) + def callback(result): + if result == wx.ID_OK: + if dlg.GetValue() != "": + if encoderType == "SAM": SAMStreamLabels[encoder.name] = dlg.GetValue() + elif encoderType == "SPL": + SPLStreamLabels[str(encoder.parent.children.index(encoder)+1)] = dlg.GetValue() + else: + if encoderType == "SAM": del SAMStreamLabels[encoder.name] + elif encoderType == "SPL": + del SPLStreamLabels[str(encoder.parent.children.index(encoder)+1)] + gui.runScriptModalDialog(dlg, callback) + # Translators: Input help mode message in SAM Encoder window. + script_streamLabeler.__doc__=_("Opens a dialog to label the selected encoder.") + + __gestures={ + "kb:f9":"connect", + "kb:f10":"disconnect", + "kb:f11":"toggleFocusToStudio", + "kb:shift+f11":"togglePlay", + "kb:f12":"streamLabeler" + } + diff --git a/addon/globalPlugins/SPLStudioUtils.py b/addon/globalPlugins/SPLStudioUtils.py index bccd4d1..154d717 100644 --- a/addon/globalPlugins/SPLStudioUtils.py +++ b/addon/globalPlugins/SPLStudioUtils.py @@ -1,27 +1,14 @@ # Station Playlist Utilities # Author: Joseph Lee # Copyright 2013-2014, released under GPL. -# Adds a few utility features such as switching focus to the SPL Studio window and some global scripts, along with support for Sam Encoder. +# Adds a few utility features such as switching focus to the SPL Studio window and some global scripts. -from ctypes import windll from functools import wraps -import threading -import os -import time -from configobj import ConfigObj # Configuration management; configspec will be used to store app module and global plugin settings in one ini file. import globalPluginHandler import api import ui -import speech -import braille -import globalVars -import review -import textInfos -from NVDAObjects.IAccessible import IAccessible import winUser import tones -import gui -import wx import addonHandler addonHandler.initTranslation() @@ -40,7 +27,7 @@ def finally_(func, final): return wrap(final) # SPL Studio uses WM messages to send and receive data, similar to Winamp (see NVDA sources/appModules/winamp.py for more information). -user32 = windll.user32 # user32.dll. +user32 = winUser.user32 # user32.dll. SPLWin = 0 # A handle to studio window. SPLMSG = winUser.WM_USER @@ -55,17 +42,6 @@ SPLLibraryScanCount = 32 SPL_TrackPlaybackStatus = 104 SPLCurTrackPlaybackTime = 105 -# Needed in SAM and SPL Encoder support: -SAMFocusToStudio = {} # A dictionary to record whether to switch to SPL Studio for this encoder. -SPLFocusToStudio = {} -SAMPlayAfterConnecting = {} -SPLPlayAfterConnecting = {} -SAMStreamLabels= {} # A dictionary to store custom labels for each stream. -SPLStreamLabels= {} # Same as above but optimized for SPL encoders (Studio 5.00 and later). - -# Configuration management. -Config = None - # Try to see if SPL foreground object can be fetched. This is used for switching to SPL Studio window from anywhere and to switch to Studio window from SAM encoder window. def fetchSPLForegroundWindow(): @@ -82,33 +58,7 @@ class GlobalPlugin(globalPluginHandler.GlobalPlugin): scriptCategory = _("Station Playlist Studio") - # Do some initialization, such as stream labels for SAM encoders. - def __init__(self): - super(globalPluginHandler.GlobalPlugin, self).__init__() - # Load stream labels (and possibly other future goodies) from a file-based database. - global config, SAMStreamLabels, SPLStreamLabels - #if os.path.isfile(os.path.join(config.getUserDefaultConfigPath(), "splStreamLabels.ini")): - config = ConfigObj(os.path.join(globalVars.appArgs.configPath, "splStreamLabels.ini")) - #else: - #config = ConfigObj(os.path.join(config.getUserDefaultConfigPath(), "splStreamLabels.ini"), create_empty=True) - # Read stream labels. - try: - SAMStreamLabels = dict(config["SAMEncoders"]) - except KeyError: - SAMStreamLabels = {} - try: - SPLStreamLabels = dict(config["SPLEncoders"]) - except KeyError: - SPLStreamLabels = {} - - # Save configuration file. - def terminate(self): - global config - config["SAMEncoders"] = SAMStreamLabels - config["SPLEncoders"] = SPLStreamLabels - config.write() - - #Global layer environment (see the app module for more information). + #Global layer environment (see the app module for more information). SPLController = False # Control SPL from anywhere. def getScript(self, gesture): @@ -253,228 +203,3 @@ class GlobalPlugin(globalPluginHandler.GlobalPlugin): #"kb:nvda+shift+`":"focusToSPLWindow", #"kb:nvda+`":"SPLControllerPrefix" } - - # Support for Sam Encoder - # Sam encoder is a Winamp plug-in, so we can use overlay class. - def chooseNVDAObjectOverlayClasses(self, obj, clsList): - fg = api.getForegroundObject() - if obj.windowClassName == "TListView" and fg.windowClassName == "TfoSCEncoders": - clsList.insert(0, self.SAMEncoderWindow) - elif obj.windowClassName == "SysListView32" and "splengine" in obj.appModule.appName: - clsList.insert(0, self.SPLEncoderWindow) - - class SAMEncoderWindow(IAccessible): - # Support for Sam Encoder window. - - # Few useful variables for encoder list: - focusToStudio = False # If true, Studio will gain focus after encoder connects. - playAfterConnecting = False # When connected, the first track will be played. - encoderType = "SAM" - - - def reportConnectionStatus(self): - # Keep an eye on the stream's description field until connected or error occurs. - # In order to not block NVDA commands, this will be done using a different thread. - SPLWin = user32.FindWindowA("SPLStudio", None) - toneCounter = 0 - while True: - time.sleep(0.001) - toneCounter+=1 - if toneCounter%250 == 0: tones.beep(500, 50) # Play status tones every second. - #info = review.getScreenPosition(self)[0] - #info.expand(textInfos.UNIT_LINE) - #if "Error" in info.text: - if "Error" in self.description: - # Announce the description of the error. - ui.message(self.description[self.description.find("Status")+8:]) - break - #elif "Encoding" in info.text or "Encoded" in info.text: - elif "Encoding" in self.description or "Encoded" in self.description: - # We're on air, so exit. - if self.focusToStudio: - fetchSPLForegroundWindow().setFocus() - tones.beep(1000, 150) - if self.playAfterConnecting: - winUser.sendMessage(SPLWin, SPLMSG, 0, SPLPlay) - break - - def script_connect(self, gesture): - gesture.send() - # Translators: Presented when SAM Encoder is trying to connect to a streaming server. - ui.message(_("Connecting...")) - # Oi, status thread, can you keep an eye on the connection status for me? - statusThread = threading.Thread(target=self.reportConnectionStatus) - statusThread.name = "Connection Status Reporter" - statusThread.start() - - def script_disconnect(self, gesture): - gesture.send() - # Translators: Presented when SAM Encoder is disconnecting from a streaming server. - ui.message(_("Disconnecting...")) - - def script_toggleFocusToStudio(self, gesture): - if not self.focusToStudio: - self.focusToStudio = True - if self.encoderType == "SAM": - SAMFocusToStudio[self.name] = True - elif self.encoderType == "SPL": - SPLFocusToStudio[str(self.IAccessibleChildID)] = True - # Translators: Presented when toggling the setting to switch to Studio when connected to a streaming server. - ui.message(_("Switch to Studio after connecting")) - else: - self.focusToStudio = False - if self.encoderType == "SAM": - SAMFocusToStudio[self.name] = False - elif self.encoderType == "SPL": - SPLFocusToStudio[str(self.IAccessibleChildID)] = False - # Translators: Presented when toggling the setting to switch to Studio when connected to a streaming server. - ui.message(_("Do not switch to Studio after connecting")) - # Translators: Input help mode message in SAM Encoder window. - script_toggleFocusToStudio.__doc__=_("Toggles whether NVDA will switch to Studio when connected to a streaming server.") - - def script_togglePlay(self, gesture): - if not self.playAfterConnecting: - self.playAfterConnecting = True - if self.encoderType == "SAM": - SAMPlayAfterConnecting[self.name] = True - elif self.encoderType == "SPL": - SPLPlayAfterConnecting[str(self.IAccessibleChildID)] = True - # Translators: Presented when toggling the setting to play selected song when connected to a streaming server. - ui.message(_("Play first track after connecting")) - else: - self.playAfterConnecting = False - if self.encoderType == "SAM": - SAMPlayAfterConnecting[self.name] = False - elif self.encoderType == "SPL": - SPLPlayAfterConnecting[str(self.IAccessibleChildID)] = False - # Translators: Presented when toggling the setting to switch to Studio when connected to a streaming server. - ui.message(_("Do not play first track after connecting")) - # Translators: Input help mode message in SAM Encoder window. - script_togglePlay.__doc__=_("Toggles whether Studio will play the first song when connected to a streaming server.") - - def script_streamLabeler(self, gesture): - curStreamLabel = "" - if self.encoderType == "SAM" and self.name in SAMStreamLabels: - curStreamLabel = SAMStreamLabels[self.name] - elif self.encoderType == "SPL" and str(self.IAccessibleChildID) in SPLStreamLabels: - curStreamLabel = SPLStreamLabels[str(self.IAccessibleChildID)] - # Translators: The title of the stream labeler dialog (example: stream labeler for 1). - streamTitle = _("Stream labeler for {streamEntry}").format(streamEntry = self.name) - # Translators: The text of the stream labeler dialog. - streamText = _("Enter the label for this stream") - dlg = wx.TextEntryDialog(gui.mainFrame, - streamText, streamTitle, defaultValue=curStreamLabel) - def callback(result): - if result == wx.ID_OK: - if dlg.GetValue() != "": - if self.encoderType == "SAM": SAMStreamLabels[self.name] = dlg.GetValue() - elif self.encoderType == "SPL": SPLStreamLabels[str(self.IAccessibleChildID)] = dlg.GetValue() - else: - if self.encoderType == "SAM": del SAMStreamLabels[self.name] - elif self.encoderType == "SPL": del SPLStreamLabels[(self.IAccessibleChildID)] - gui.runScriptModalDialog(dlg, callback) - # Translators: Input help mode message in SAM Encoder window. - script_streamLabeler.__doc__=_("Opens a dialog to label the selected encoder.") - - - def initOverlayClass(self): - # Can I switch to Studio when connected to a streaming server? - try: - self.focusToStudio = SAMFocusToStudio[self.name] - except KeyError: - pass - - def event_gainFocus(self): - try: - streamLabel = SAMStreamLabels[self.name] - except KeyError: - streamLabel = None - # Speak the stream label if it exists. - if streamLabel is not None: speech.speakMessage(streamLabel) - super(type(self), self).reportFocus() - # Braille the stream label if present. - if streamLabel is not None: - brailleStreamLabel = self.name + ": " + streamLabel - braille.handler.message(brailleStreamLabel) - - - __gestures={ - "kb:f9":"connect", - "kb:f10":"disconnect", - "kb:f11":"toggleFocusToStudio", - "kb:shift+f11":"togglePlay", - "kb:f12":"streamLabeler" - } - - class SPLEncoderWindow(SAMEncoderWindow): - # Support for SPL Encoder window. - - # A few more subclass flags. - encoderType = "SPL" - - def reportConnectionStatus(self): - # Same routine as SAM encoder: use a thread to prevent blocking NVDA commands. - SPLWin = user32.FindWindowA("SPLStudio", None) - attempt = 0 - while True: - time.sleep(0.001) - attempt += 1 - if attempt%250 == 0: tones.beep(500, 50) - #info = review.getScreenPosition(self)[0] - #info.expand(textInfos.UNIT_LINE) - #if not info.text.endswith("Disconnected"): ui.message(info.text) - #if info.text.endswith("Connected"): - if "Unable to connect" in self.name: - break - if self.name.endswith("Connected"): - # We're on air, so exit. - if self.focusToStudio: - fetchSPLForegroundWindow().setFocus() - tones.beep(1000, 150) - if self.playAfterConnecting: - winUser.sendMessage(SPLWin, SPLMSG, 0, SPLPlay) - break - if not self.name.endswith("Connected"): ui.message(self.name[self.name.find("Transfer")+15:]) - - def script_connect(self, gesture): - # Same as SAM's connection routine, but this time, keep an eye on self.name and a different connection flag. - connectButton = api.getForegroundObject().children[2] - if connectButton.name == "Disconnect": return - ui.message(_("Connecting...")) - # Juggle the focus around. - connectButton.doAction() - self.setFocus() - # Same as SAM encoders. - statusThread = threading.Thread(target=self.reportConnectionStatus) - statusThread.name = "Connection Status Reporter" - statusThread.start() - script_connect.__doc__=_("Connects to a streaming server.") - - - def initOverlayClass(self): - # Can I switch to Studio when connected to a streaming server? - try: - self.focusToStudio = SPLFocusToStudio[str(self.IAccessibleChildID)] - except KeyError: - pass - - def event_gainFocus(self): - try: - streamLabel = SPLStreamLabels[str(self.IAccessibleChildID)] - except KeyError: - streamLabel = None - # Speak the stream label if it exists. - if streamLabel is not None: speech.speakMessage(streamLabel) - super(type(self), self).reportFocus() - # Braille the stream label if present. - if streamLabel is not None: - brailleStreamLabel = str(self.IAccessibleChildID) + ": " + streamLabel - braille.handler.message(brailleStreamLabel) - - - - __gestures={ - "kb:f9":"connect", - "kb:f10":None - } - Repository URL: https://bitbucket.org/nvdaaddonteam/stationplaylist/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email.