Hi,
Now that we've visited internals of StationPlaylist Studio add-on, I'd like to give you a tour of my lab where I develop this add-on. Along the way you'll learn how an add-on is born, coded, tested, released and maintained.
Lab setup, development equipment and software
For all my software development, I use two computers: a touchscreen laptop and a desktop, both running Windows 10 and latest NVDA next branch snapshots. Both also run Cygwin to run various command-line tools (Git, SCons, etc.), and in case I need to compile NVDA from source code, installed Visual Studio 2012 with latest update and other dependencies.
In case of SPL add-on, I have different Studio versions installed: 5.01 on my laptop and 5.10 on the desktop. This allows me to work on both versions at once (both computers have the full source code of the add-on, though I tend to write bug fixes on my laptop and experiment with new things on my desktop).
Git: a "smart" source code manager
Like other NVDA developers and many add-on writers, I use Git for source code management (contrary to its slogan, Git is very smart). This is a distributed system, meaning a local repository contains the complete record of how the source code is managed (no need to connect to a server to commit and fetch updates). For example, using just my local copy of the SPL add-on source code, I can view commit history and generate older add-on releases.
Another advantage of Git is extensive support for branches. A branch is a development workflow separate from other branches. For example, NVDA screen reader uses at least three branches for its workflow: next (alpha), master (live beta) and rc (release candidate, used to build official releases). SPL add-on uses this approach as well: there are at least two branches in use, called master and stable used for ongoing development or release and maintenance, respectively (we'll come back to branches in a second).
How an add-on feature is born
Let's go through a typical development process for an add-on feature by looking at how broadcast profiles was developed (for more information on broadcast profiles, refer to configuration management article).
I started working on broadcast profiles in March 2015 while developing add-on 5.0. This was a natural extension of add-on settings dialog: whereas this dialog (and the configuration database it uses) only dealt with a single profile, I thought it might be a good idea to allow multiple profiles to be defined and to let the settings dialog respond to profile changes.
There was an important reason for writing this feature: Since NVDA supports multiple configuration profiles and since some broadcasters were hosting multiple shows, I thought it would be a good idea to implement a similar feature in the SPL add-on. Thus, I envisioned broadcast profiles to be used primarily by people hosting multiple shows, with each show defined as a profile.
In March and April 2015, I started rewriting certain parts of add-on configuration manager (splstudio.splconfig) in preparation for developing broadcast profiles (now included as part of add-on 6.0-dev). I started by writing todo comments (where appropriate) describing what the future feature should be like. I then modified initConfig and saveConfig (discussed in app module articles), initially telling them to work with the default profile (the one and only configuration map then), then I left it alone until add-on 5.0 was released in June 2015.
In June 2015, I opened a new branch (initially using the codename "starfish") to house code related to broadcast profiles. Before any "real" code was written, I studied NvDA source code dealing with configuration profiles to learn more about how Jamie (James Teh from NV Access) implemented this feature. Once I understood how it worked, I copied, pasted and changed the code to match the overall add-on code base (giving nV Access the credit they deserve).
One of the first things I had to decide was how to store profiles. I experimented with using ConfigObj sections, one per profile, but this proved to be problematic (a profile could be given the name of an existing configuration map key). I then went back to NVDA source code to find out how NV Access solved this problem (using separate ini files), implemented it, and was met with another problem: transfering values between profiles. This was resolved by specifying whether a setting was "global" (applies to all profiles) or specific to a profile. Next came profile controls in the add-on settings dialog and using choice events to set alarm values using values from the selected profile.
The last thing I did before merging the broadcast profiles branch to master branch in July was revising configuration error dialog and writing documentation for broadcast profiles. Once the documentation was ready and small issues were fixed after going through many rounds of testing (on my own computer and from the profiles branch itself), broadcast profiles branch was merged into master. But the development didn't stop there: thanks to provisions I made, it was quite simple to implement instant switch profiles (again it had issues which are now largely resolved).
Dealing with threaded code: headaches during development of background encoder monitoring feature
You may recall our discussion of Cart Explorer and how it went through extensive testing to arrive at the current state (this was a difficult code segment). When it comes to difficulty, nothing beats multithreaded code, especially if it involves multiple threads working in parallel (rather, almost parallel), and I tasted this when writing background encoder monitor (add-on 5.0). This involved tracking how many threads were running to make sure no unnecessary threads were running, catching suttle errors and race conditions (a connection attempt could run a thread without checking if the encoder is being monitored) and so on. Thankfully, I went through a similar set of problems a few months earlier when I struggled with library scan (add-on 4.0), and that experience taught me to be careful with threads (and to experience fewer headaches).
Add-on development process
Follow me as I show you how a typical SPL add-on version is developed, released and maintained:
1. Before starting work on the new add-on version, I write down some goals the add-on should achieve, including feature ideas, user (your) suggestions and so on.
2. I then hold a conference call with add-on users to see what they think about some ideas and gather feedback (these are also written down).
3. I then create separate branches for each feature in order to isolate code and not to break existing code.
4. Next, I write todo comments reminding myself as to what the feature should be like, then I start working on it. As each feature is being developed, I do mental simulations as to how you might use the feature under development, such as possible errors, messages spoken and so on.
5. Once the feature is quite stable, I test the feature to uncover bugs and to fill in the missing pieces. When it comes to testing, I test the new feature branch on both of my computers running different versions of Studio to make sure it works across versions (if not, I go back and modify the code to recognize differences between Studio versions).
6. After testing the feature for a while and if the feature is stable, I merge the feature branch into master.
7. Every few weeks, I publish master branch snapshots to gather feedback from users willing to test drive snapshots.
8. At some point, I set release target window for the next add-on version (for 6.0, it is January 2016). This determines when feature freeze should be and beta release window (for 6.0, beta is scheduled for sometime in December 2015). Between feature freeze and the first beta release, I concentrate on code refinements and bug fixes.
9. After going through several beta cycles (typically two), I ask NVDA community add-on reviewers to review my add-on code and request add-on release during the release window (this is done by merging master branch into stable branch).
10. Once the add-on version is released, subsequent maintenance versions (localization updates, bug fixes, minor tweaks) will be released from the stable branch, with the master branch containing the code for the next major version.
11. Once the next version enters beta cycle, further maintenance releases may or may not happen (an exception is long term support release, described below).
Long term support release
A typical add-on version is supported until the next add-on version is released (currently several months). However, there are times when an add-on version receives extended support (termed long term support (LTS) release). This happens if the next major version of Studio is released or a version of Studio with user interface changes is released.
A LTS version is a major version of the SPL add-on with some notable differences:
* Support duration: A LTS version is supported for at least two add-on development cycles (at least a year).
* Features: A LTS version may contain some features from the next major add-on release.
* Studio version supported: A LTS version is the last version to support the oldest supported Studio version. This is designed to give people plenty of time to upgrade to newer Studio releases.
As of August 2015, the most recent (and so far the only) LTS version was add-on 3.x (September 2014 to June 2015). Add-on 3.x was maintained thus:
1. Add-on 3.0 was released in September 2014.
2. Add-on 3.5 could have been the last maintenance version for add-on 3.x if it was not a LTS version.
3. When add-on 4.0 was released (January 2015), add-on 3.6 was released, backporting some features from 4.0. Users were told that add-on 3.x will be the last version to support Studio versions earlier than 5.00. From that time on, add-on 3.x was taken off the stable branch and was moved to an internal branch.
4. When add-on 5.0 beta was released (May 2015), add-on 3.x (3.9 was available then) entered end of support countdown (no more maintenance releases).
5. A few weeks later, when add-on 5.0 came out (June 2015), add-on 3.x became unsupported.
Final thoughts
As I close this series of articles on StationPlaylist Studio Add-on Internals, I feel it is time I reveal why my add-ons are free: it is because I love you users and as a service for NVDA user and developer community (and in extension, to all blind broadcasters using SPL Studio). What brings me joy as an add-on writer is the fact that this add-on (and accompanying documentation) has made impact in your lives and lives of listeners to your shows, as well as to other NVDA users and developers around the world. Thank you users for your continued support and feedback, and I promise once again that all my add-on code (including SPL Studio add-on) will be free and anyone is welcome to study and improve upon it.
For add-on writers looking for quality add-on documentation, I hope this series gave you an inspiration as to how to write amazing documentation in your future projects. For people new to add-on writing or for those interested in writing an add-on, I hope this Add-ons Internals series served as a handy resource for your projects, and in extension, gave you an idea as to how certain NVDA functions work. If you'd like to reference this documentation or use it as a blueprint, you are more than welcome to do so. Thank you community add-on reviewers for your continued support and reviews.
Important notices and credits
I'd like to thank StationPlaylist staff for continued collaboration with screen reader users in regards to accessibility of Studio. A special thanks goes to Jamie Teh from NV Access and Geoff Shang (original add-on author) for giving me and others a foundation for future goodies. As always, the biggest thanks goes to you, the users of SPL add-on for your continued feedback and teaching me new things about studio.
Source code notice: to protect copyrights, parts of Studio API has not been documented. Also, source code discussed throughout this series may change as future add-on versions are developed.
Copyrights: StationPlaylist Studio, Track Tool and StationPlaylist Encoders are copyright StationPlaylist.com. NonVisual Desktop Access is copyright 2006-2015 NV access Limited (released under GPL). SAM Encoders is copyright Spatial Audio. Microsoft Windows and Windows API are copyright Microsoft Corporation. Python is copyright Python Software Foundation. StationPlaylist Studio add-on for NvDA is copyright 2011, 2013-2015 Geoff Shang, Joseph Lee and others (released under GPL). Other products mentioned are copyrighted by owners of these products (licenses vary).
Wednesday, August 26, 2015
StationPlaylist Add-on Internals: encoder support
Hi,
We have now arrived at the penultimate article in this Add-on Internals series for StationPlaylist add-on: encoder support, the second pillar for the SPL Utilities global plugin. We'll talk about how encoder support is implemented, how NVDA can detect stream labels and a behind the scenes overview of what happens when you connect to a streaming server.
Encoder support: From suggestion to implementation
Originally, I wasn't planning on including encoder support into the SPL add-on. However, after talking to some Studio users who were using SAM encoders and seeing how other screen readers supported it, I decided to investigate SAM encoder support in summer 2014.
The first issue I had to solve was making NVDA recognize the encoder entries themselves. Once that was solved, the next task was announcing connection error messages, which led to figuring out how SAM encoders react when connected to a streaming server.
Originally, I manipulated text written to the screen to obtain needed status messages (via text infos). This routine caused some to experience screen flickering issues when connecting to a streaming server. This was resolved by using encoder description (obj.description), which opened up a possibility to monitor changes to this text via a background thread (more on this routine below), which also eliminated a need to stay on the encoders window until connected.
While I was resolving problems with SAM encoders, I also worked on refactoring encoder support code to support StationPlaylist encoders (add-on 4.0). Initially, encoder support code was optimized for SAM encoders, but the current code structure (explained below) was written to extend basid encoder support easily, and as a result, both SAM and SPL encoder entries present similar interfaces and commands.
Encoder entries: Yet another overlay class family
Just like Studio track items (see the article on track items), encoder entries are overlay classes. Each encoder type (SAM or SPL) inherits from a single encoder object (SPLStudioUtils.encoders.EncoderWindow) that provides basic services such as settings commands, announcing stream labels and so on. Then each encoder type adds encoder-specific routines such as different connection detection routines, ways of obtaining stream labels and so on. Speaking of stream labels and settings, the base encoder class is helped by some friends from the encoder module itself, including a configuration map to store stream labels and basic settings, a routine to obtain encoder ID (encoder string and the IAccessible child ID) and so on.
On top of the base encoder class are two encoder classes, representing SAM encoder entries and SPL encoder entries. SAM encoder entries (SPLStudioUtils.encoders.SAMEncoderWindow) is laid out just like Studio's track items, whereas SPL encoder entries (SPLStudioUtils.encoders.SPLEncoderWindow) is a typical SysListView32 control (see an article on column routines for more information). Both classes provide similar routines, with the only difference being how connection messages are handled.
Common services: basic settings, stream labels and related methods
All encoder classes provide the following common services:
* Configuring settings: three settings can be configured:
A. Pressing F11 will tell NVDA if NVDA snuld switch to Studio when the encoder is connected.
B. Pressing Shift+F11 will ask Studio will play the next track when connected.
C. Pressing Control+F11 will enable background encoder monitoring (more on this in a second).
D. Once these settings are changed, the new values will be stored in appropriate flag in the encoder entry, which in turn are saved in the configuration map.
* Applies settings. This is done by initOverlayClass method - once called, this method will look up various settings for the encoder from the configuration map (key is the setting flag, value is the encoder ID). Another job of this routine is to load stream labels when an encoder first gains focus (if this was loaded earlier, it could be a waste of space, especially if encoders are never used).
* Announces stream labels (if defined) via reportFocus method. In contrast with the Studio track item version, an encoder's reportFocus routine:
A. Locates stream labels for the current encoder (the configuration map stores stream labels as dictionaries (sections), with each dictionary representing the encoder type, key is the encoder position and the value is the label; each encoder, when told to look up stream labels, will consult its own labels dictionary).
B. If a label is found, NVDA will announce the label (in braille, surrounded by parentheses).
* Define and remove stream labels. This is done via stream labels dialog (F12) that'll make sure you entered a label (if not, the encoder position is removed from the encoder-specific stream labels dictionary).
* Updates stream label position when told to do so (via a dialog, activated by pressing Control+F12). This is needed if encoders were removed, as you may hear stream label for an encoder that no longer exists. This is implemented as a variation of find predecessor algorithm.
* Announces encoder columns. The base class can announce encoder position (Control+NVDA+1) and stream label (Control+NVDA+2), while SAM can announce encoder format, status and description and SPL allows one to hear encoder format and transfer rate/connection status.
Encoder ID's
An encoder ID is a string which uniquely identifies an encoder. This consists of a string denoting the encoder type (SAM for SAM encoder, for instance), followed by the encoder position (separated by a space). For instance, the first SAM encoder is given the ID "SAM 1". The ID's are used to look up stream labels, configure settings and to identify encoders being monitored (SPL Controller, E).
More and more threads: connection messages and background encoder monitoring
As we saw in a previous article, threads allow developers to let programs perform certain tasks in the background. Even in encoder support, threads are employed for various tasks, including connection message announcement and background encoder monitoring.
Each encoder overlay class (not the base encoder) includes dedicated connection handling routines (reportConnectionStatus). Depending on how you invoke this, it starts up as follows:
* If background encoder monitoring is off and you press F9 to connect, NVDA will run this routine in a separate thread. For SAM, this is checked right after sending F9 to the application, and for SPL, this is done after clicking "connect" button (manipulates focus in the process).
* If background encoder monitoring is on before pressing F9, the routine will run from another thread when this setting is switched on. Then when you press F9, NvDA knows that the background monitoring thread is active, thus skipping the above step.
The connection handling routine performs the following:
1. Locates status message for the encoder entry. For SAM, it is the description text, and for SPL, it is one of the entry's child objects (columns). This will be done as long as Studio and/or NVDA is live (that is, if the thread is running).
2. Announces error messages if any and will try again after waiting a little while (fraction of a second).
3. If connected, NvDA will play a tone, then:
A. Do nothing if not told to focus to studio nor play the next track.
B. Focuses to studio and/or plays the next track if no tracks are playing.
4. For other messages, NVDA will periodically play a progress tone and announce connection status so far as reported by the encoder.
5. This loop repeats as long as this encoder is being monitored in the background.
Encoder-specific routines
In addition to basic services, each encoder routine has its own goodies, including:
For SAM encoders:
* To disconnect, press F10.
* You can press Control+F9 or Control+F10 to connect or disconnect all encoders (does not work well in recent SAM releases, according to my tests).
For SPL encoders:
* When you press F9 to connect, NVDA does the following:
1. Locates "connect" button, and if it says "Connect", clicks it (obj.doAction).
2. Moves focus back to the entry (self.SetFocus).
* To disconnect, press TAB until you arrive at "Disconnect" button and press SPACE.
Conclusion
We've come a long way: from add-on design and app module contents to encoder support, we discussed internals of a project that makes a difference in lives of many blind broadcasters around the world. I learned many things developing this add-on, and I hope to improve this add-on in the future (which also means publishing future editions of this internals series as new add-on versions are released). To put it all together, let me give you a tour around my lab where I think about, code, test and talk about StationPlaylist Studio add-on for NVDA (and in extension, NVDA screen reader itself).
We have now arrived at the penultimate article in this Add-on Internals series for StationPlaylist add-on: encoder support, the second pillar for the SPL Utilities global plugin. We'll talk about how encoder support is implemented, how NVDA can detect stream labels and a behind the scenes overview of what happens when you connect to a streaming server.
Encoder support: From suggestion to implementation
Originally, I wasn't planning on including encoder support into the SPL add-on. However, after talking to some Studio users who were using SAM encoders and seeing how other screen readers supported it, I decided to investigate SAM encoder support in summer 2014.
The first issue I had to solve was making NVDA recognize the encoder entries themselves. Once that was solved, the next task was announcing connection error messages, which led to figuring out how SAM encoders react when connected to a streaming server.
Originally, I manipulated text written to the screen to obtain needed status messages (via text infos). This routine caused some to experience screen flickering issues when connecting to a streaming server. This was resolved by using encoder description (obj.description), which opened up a possibility to monitor changes to this text via a background thread (more on this routine below), which also eliminated a need to stay on the encoders window until connected.
While I was resolving problems with SAM encoders, I also worked on refactoring encoder support code to support StationPlaylist encoders (add-on 4.0). Initially, encoder support code was optimized for SAM encoders, but the current code structure (explained below) was written to extend basid encoder support easily, and as a result, both SAM and SPL encoder entries present similar interfaces and commands.
Encoder entries: Yet another overlay class family
Just like Studio track items (see the article on track items), encoder entries are overlay classes. Each encoder type (SAM or SPL) inherits from a single encoder object (SPLStudioUtils.encoders.EncoderWindow) that provides basic services such as settings commands, announcing stream labels and so on. Then each encoder type adds encoder-specific routines such as different connection detection routines, ways of obtaining stream labels and so on. Speaking of stream labels and settings, the base encoder class is helped by some friends from the encoder module itself, including a configuration map to store stream labels and basic settings, a routine to obtain encoder ID (encoder string and the IAccessible child ID) and so on.
On top of the base encoder class are two encoder classes, representing SAM encoder entries and SPL encoder entries. SAM encoder entries (SPLStudioUtils.encoders.SAMEncoderWindow) is laid out just like Studio's track items, whereas SPL encoder entries (SPLStudioUtils.encoders.SPLEncoderWindow) is a typical SysListView32 control (see an article on column routines for more information). Both classes provide similar routines, with the only difference being how connection messages are handled.
Common services: basic settings, stream labels and related methods
All encoder classes provide the following common services:
* Configuring settings: three settings can be configured:
A. Pressing F11 will tell NVDA if NVDA snuld switch to Studio when the encoder is connected.
B. Pressing Shift+F11 will ask Studio will play the next track when connected.
C. Pressing Control+F11 will enable background encoder monitoring (more on this in a second).
D. Once these settings are changed, the new values will be stored in appropriate flag in the encoder entry, which in turn are saved in the configuration map.
* Applies settings. This is done by initOverlayClass method - once called, this method will look up various settings for the encoder from the configuration map (key is the setting flag, value is the encoder ID). Another job of this routine is to load stream labels when an encoder first gains focus (if this was loaded earlier, it could be a waste of space, especially if encoders are never used).
* Announces stream labels (if defined) via reportFocus method. In contrast with the Studio track item version, an encoder's reportFocus routine:
A. Locates stream labels for the current encoder (the configuration map stores stream labels as dictionaries (sections), with each dictionary representing the encoder type, key is the encoder position and the value is the label; each encoder, when told to look up stream labels, will consult its own labels dictionary).
B. If a label is found, NVDA will announce the label (in braille, surrounded by parentheses).
* Define and remove stream labels. This is done via stream labels dialog (F12) that'll make sure you entered a label (if not, the encoder position is removed from the encoder-specific stream labels dictionary).
* Updates stream label position when told to do so (via a dialog, activated by pressing Control+F12). This is needed if encoders were removed, as you may hear stream label for an encoder that no longer exists. This is implemented as a variation of find predecessor algorithm.
* Announces encoder columns. The base class can announce encoder position (Control+NVDA+1) and stream label (Control+NVDA+2), while SAM can announce encoder format, status and description and SPL allows one to hear encoder format and transfer rate/connection status.
Encoder ID's
An encoder ID is a string which uniquely identifies an encoder. This consists of a string denoting the encoder type (SAM for SAM encoder, for instance), followed by the encoder position (separated by a space). For instance, the first SAM encoder is given the ID "SAM 1". The ID's are used to look up stream labels, configure settings and to identify encoders being monitored (SPL Controller, E).
More and more threads: connection messages and background encoder monitoring
As we saw in a previous article, threads allow developers to let programs perform certain tasks in the background. Even in encoder support, threads are employed for various tasks, including connection message announcement and background encoder monitoring.
Each encoder overlay class (not the base encoder) includes dedicated connection handling routines (reportConnectionStatus). Depending on how you invoke this, it starts up as follows:
* If background encoder monitoring is off and you press F9 to connect, NVDA will run this routine in a separate thread. For SAM, this is checked right after sending F9 to the application, and for SPL, this is done after clicking "connect" button (manipulates focus in the process).
* If background encoder monitoring is on before pressing F9, the routine will run from another thread when this setting is switched on. Then when you press F9, NvDA knows that the background monitoring thread is active, thus skipping the above step.
The connection handling routine performs the following:
1. Locates status message for the encoder entry. For SAM, it is the description text, and for SPL, it is one of the entry's child objects (columns). This will be done as long as Studio and/or NVDA is live (that is, if the thread is running).
2. Announces error messages if any and will try again after waiting a little while (fraction of a second).
3. If connected, NvDA will play a tone, then:
A. Do nothing if not told to focus to studio nor play the next track.
B. Focuses to studio and/or plays the next track if no tracks are playing.
4. For other messages, NVDA will periodically play a progress tone and announce connection status so far as reported by the encoder.
5. This loop repeats as long as this encoder is being monitored in the background.
Encoder-specific routines
In addition to basic services, each encoder routine has its own goodies, including:
For SAM encoders:
* To disconnect, press F10.
* You can press Control+F9 or Control+F10 to connect or disconnect all encoders (does not work well in recent SAM releases, according to my tests).
For SPL encoders:
* When you press F9 to connect, NVDA does the following:
1. Locates "connect" button, and if it says "Connect", clicks it (obj.doAction).
2. Moves focus back to the entry (self.SetFocus).
* To disconnect, press TAB until you arrive at "Disconnect" button and press SPACE.
Conclusion
We've come a long way: from add-on design and app module contents to encoder support, we discussed internals of a project that makes a difference in lives of many blind broadcasters around the world. I learned many things developing this add-on, and I hope to improve this add-on in the future (which also means publishing future editions of this internals series as new add-on versions are released). To put it all together, let me give you a tour around my lab where I think about, code, test and talk about StationPlaylist Studio add-on for NVDA (and in extension, NVDA screen reader itself).
StationPlaylist Add-on Internals: introduction to SPL Controller layer and focus to studio window routine
Hi,
Now that we've covered the "kernel" (innermost parts) of the Studio add-on, it is time to talk about the icing: SPL Utilities global plugins and its contents. The next few articles will talk about what the global plugin does, introduce you to inner workings of SPL Controller layer and tour how encoder support is implemented.
Studio app module versus SPL Utilities global plugin
As described in the add-on design article, SPL Studio add-on comes with two app modules and a global plugin. This was needed not only to differentiate between module types and expertese, but also allow Studio functions to be invoked from other programs. With the introduction of encoder support in add-on 3.0 (fall 2014), the global plugin portion of the add-on (SPL Utilities) took on an important role: working as an encoders manager to report connection status and to perform other routines.
SPL Utilities package contents
The SPL Utilities global plugin consists of the following modules:
* Main plugin code (__init__.py), containing essential global routines such as SPL Controller (described below) and a procedure to focus to Studio window upon request. This module defines constants used by Studio to receive messages, a function to focus to Studio window and the global plugin class containing definitions for SPL Controller layer commands.
* Encoder support (encoders.py), outlining NVDA's support for various encoders (see the next article; the main global plugin module plays an important part in helping the encoder module as you'll see in the next article).
SPL Controller layer
The SPL Controller layer (entry command unassigned, same reason as the Assistant layer entry command) is used to invoke Studio functions from anywhere. The entry routine is similar to the app module counterpart (SPL Assistant) except for the following:
* NVDA will make sure Studio is running (if so, it'll cache the window handle value just as in the Studio app module), otherwise it cannot enter SPL Controller layer.
* All commands (except two) use Studio API (Studio API and use of user32.dll's SendMessage was described in a previous article).
For mechanics of layer commands, see a previous article on add-on design where layer commands were discussed.
The following commands utilize Studio API:
* A/Shift+A: Automation on/off.
* L/Shift+L: Line in on/off.
* M/Shift+M/N: Microphone on/off/instant on/off toggle.
* P: Play.
* R: Remaining time for the currently playing track (if any).
* Shift+R: Library scan progress and umber of items scanned.
* S/T: Stop with fade/instant stop.
* U: Play/pause.
For readers familiar with Studio keyboard commands, you'll find yourself at home (they are indeed Studio commands except pressing Shift will turn a feature off and Shift+R will remind you of Control+Shift+R for library scan from Insert Tracks dialog).
Here are the two exceptions
* E: If you tell NVDA to monitor one or more encoders in the background, this command will announce number of encoders being monitored (see the next aritlce on the format of this message).
* F1: Opens a dialog displaying Controller layer commands (does this sound familiar?).
Focusing to Studio window from anywhere
As you are broadcasting a show with Studio, you may find yourself in a situation where you need to switch to Studio quickly to take care of automation, insert new tracks and so on. An ideal situation is to switch to Studio when you press Alt+TAB (this isn't the case if you have more than two programs opened). For this reason, screen reader scripts for Studio includes a command to switch to Studio upon request (unassigned in NVDA).
In NVDA world, this is accomplished with a function in the SPL Utilities module (SPLStudioUtils.fetchSPLForegroundWindow). This is employed not only by the main global plugin module (called from a script to focus to Studio window), but also used in encoders for various functions. The routine is as follows:
1. The focus to Studio script will check if Studio is running, and if so, it'll call the fetch window function, which in turn locates the desktop (shell) window to serve as the starting point for locating Studio window.
2. NVDA will scan top-level windows (children of desktop object) until a Studio window (where the window's app module is the Studio app module) is found, and if found, NVDA will increment a Studio window candidate counter.
3. Once top-level window scanning is complete, NVDA will take action based on what the Studio window candidate counter says before passing the foreground object back to the main script. It can do one of the following:
A. If counter is 0 (fg is None), NVDA will know that you have minimized Studio, so it'll tell you that Studio is minimized.
B. If counter is 1, NVDA will locate the Studio window by looking for the main Studio window (user32.dll is involved).
C. For all other values, NVDA will assume the last window found is the Studio window (held in fg variable) and return it.
4. Back at the focus to Studio script, NvDA will either announce if Studio is minimized or switch to the foreground window returned by the fetch window function (fg.SetFocus).
Conclusion
The routines discussed above (SPL Controller and the command to switch to Studio window) is one of the two pillars of the SPL Studio Utilities global plugin (the other is encoder support). With these routines, it became possible to perform playback operations without focusing to studio, and you can switch to Studio window from anywhere, anytime. We'll visit the other side of this global plugin story in the next StationPlaylist Add-on Internals article, and after that, we'll conclude with an interview with the maintainer of the add-on to learn about how he (I) develop new add-on features.
Now that we've covered the "kernel" (innermost parts) of the Studio add-on, it is time to talk about the icing: SPL Utilities global plugins and its contents. The next few articles will talk about what the global plugin does, introduce you to inner workings of SPL Controller layer and tour how encoder support is implemented.
Studio app module versus SPL Utilities global plugin
As described in the add-on design article, SPL Studio add-on comes with two app modules and a global plugin. This was needed not only to differentiate between module types and expertese, but also allow Studio functions to be invoked from other programs. With the introduction of encoder support in add-on 3.0 (fall 2014), the global plugin portion of the add-on (SPL Utilities) took on an important role: working as an encoders manager to report connection status and to perform other routines.
SPL Utilities package contents
The SPL Utilities global plugin consists of the following modules:
* Main plugin code (__init__.py), containing essential global routines such as SPL Controller (described below) and a procedure to focus to Studio window upon request. This module defines constants used by Studio to receive messages, a function to focus to Studio window and the global plugin class containing definitions for SPL Controller layer commands.
* Encoder support (encoders.py), outlining NVDA's support for various encoders (see the next article; the main global plugin module plays an important part in helping the encoder module as you'll see in the next article).
SPL Controller layer
The SPL Controller layer (entry command unassigned, same reason as the Assistant layer entry command) is used to invoke Studio functions from anywhere. The entry routine is similar to the app module counterpart (SPL Assistant) except for the following:
* NVDA will make sure Studio is running (if so, it'll cache the window handle value just as in the Studio app module), otherwise it cannot enter SPL Controller layer.
* All commands (except two) use Studio API (Studio API and use of user32.dll's SendMessage was described in a previous article).
For mechanics of layer commands, see a previous article on add-on design where layer commands were discussed.
The following commands utilize Studio API:
* A/Shift+A: Automation on/off.
* L/Shift+L: Line in on/off.
* M/Shift+M/N: Microphone on/off/instant on/off toggle.
* P: Play.
* R: Remaining time for the currently playing track (if any).
* Shift+R: Library scan progress and umber of items scanned.
* S/T: Stop with fade/instant stop.
* U: Play/pause.
For readers familiar with Studio keyboard commands, you'll find yourself at home (they are indeed Studio commands except pressing Shift will turn a feature off and Shift+R will remind you of Control+Shift+R for library scan from Insert Tracks dialog).
Here are the two exceptions
* E: If you tell NVDA to monitor one or more encoders in the background, this command will announce number of encoders being monitored (see the next aritlce on the format of this message).
* F1: Opens a dialog displaying Controller layer commands (does this sound familiar?).
Focusing to Studio window from anywhere
As you are broadcasting a show with Studio, you may find yourself in a situation where you need to switch to Studio quickly to take care of automation, insert new tracks and so on. An ideal situation is to switch to Studio when you press Alt+TAB (this isn't the case if you have more than two programs opened). For this reason, screen reader scripts for Studio includes a command to switch to Studio upon request (unassigned in NVDA).
In NVDA world, this is accomplished with a function in the SPL Utilities module (SPLStudioUtils.fetchSPLForegroundWindow). This is employed not only by the main global plugin module (called from a script to focus to Studio window), but also used in encoders for various functions. The routine is as follows:
1. The focus to Studio script will check if Studio is running, and if so, it'll call the fetch window function, which in turn locates the desktop (shell) window to serve as the starting point for locating Studio window.
2. NVDA will scan top-level windows (children of desktop object) until a Studio window (where the window's app module is the Studio app module) is found, and if found, NVDA will increment a Studio window candidate counter.
3. Once top-level window scanning is complete, NVDA will take action based on what the Studio window candidate counter says before passing the foreground object back to the main script. It can do one of the following:
A. If counter is 0 (fg is None), NVDA will know that you have minimized Studio, so it'll tell you that Studio is minimized.
B. If counter is 1, NVDA will locate the Studio window by looking for the main Studio window (user32.dll is involved).
C. For all other values, NVDA will assume the last window found is the Studio window (held in fg variable) and return it.
4. Back at the focus to Studio script, NvDA will either announce if Studio is minimized or switch to the foreground window returned by the fetch window function (fg.SetFocus).
Conclusion
The routines discussed above (SPL Controller and the command to switch to Studio window) is one of the two pillars of the SPL Studio Utilities global plugin (the other is encoder support). With these routines, it became possible to perform playback operations without focusing to studio, and you can switch to Studio window from anywhere, anytime. We'll visit the other side of this global plugin story in the next StationPlaylist Add-on Internals article, and after that, we'll conclude with an interview with the maintainer of the add-on to learn about how he (I) develop new add-on features.
Monday, August 24, 2015
StationPlaylist Add-on Internals: configuration management, settings dialog and broadcast profiles
Hi,
We have arrived at our last station stop for Studio app module internals: configuration management. This facility allows a broadcaster to configure various options such as alarms, column announcement order and so on, as well as package settings for a show as a broadcast profile to be invoked during the show. Along the way you'll learn how NVDA screen reader stores various settings, what happens if something goes wrong and get into internals of how broadcast profiles work.
ConfigObj: NVDA's configuration manager assistant
NVDA uses ConfigObj library to manage settings. This Python module, inspired by Python's own Config Parser, allows developers to store settings in a text file, read and interpret settings and validate options against default configuration options.
NVDA comes with a collection of default options. They live in source/config/__init__ and are used for various things, including presenting preferences, validating user configuration and so on. The config management module also includes facilities to handle profiles (a package of user settings to be used in an app, during say all or reserved for manual activation).
NVDA configuration profiles: multiple configuration files, one online database
A number of users asked NV Access if it would be possible to have profiles where certain settings can take effect while one is using apps or during say all. NV Access listened and introduced configuration profiles in late 2013. As of August 2015, one can create a manual or an automated (triggered) profile, with the latter further divided into say all profile and app-specific one.
Configuration profiles involve a few support routines and a careful coordination between configuration files. In essence, each configuration profile (stored in profiles folder in user configuration folder) is a snapshot of differences between the profile and the main user configuration file (named nvda.ini). When a profile becomes active, NVDA will load the profile file associated with the given profile and modify user settings according to values stored in the newly activated profile, and wwill record the name of the profile file to remind itself as to which profile is active (the default user configuration profile is named "normal configuration" with the file name of nvda.ini).
What if settings had errors? As part of the startup routine (portions of main function (source/core.py) prior to entering the main loop), NVDA will display a configuration error dialog if it detects serious issues with configuration values (in reality, ConfigObj notifies NVDA of this problem). You'll see this is also implemented in the Studio app module to deal with add-on configuration issues.
All about Studio add-on Configuration Manager
Until a few months ago, Studio app module handled all add-on configuration routines. With the advent of add-on 5.0 which introduced add-on settings dialog, configuration management routines were split into a dedicated Configuration Manager (splstudio.splconfig). The new module takes care of configuration routines, including validating the user configuration, presenting add-on settings dialog and other dialogs inside it, handling broadcast profiles and more. We'll cover each routine in this article.
How settings are loaded, used and saved
As mentioned in an article on life of the Studio app module, one of the things the app module does is load the add-on configuration database by calling splconfig.initConfig function. The job of this function is to load the add-on configuration map from various places (for add-on 5.x, it will be the main configuration map only, while 6.0 also searches appModules/profiles folder to load broadcast profiles). The format of the configuration file is that of a typical ini file, and as far as NVDA is concerned, it is a dictionary.
When the configuration database is ready, Studio app module will then use values stored in this settings dictionary to perform various tasks, including microphone alarm, announcing listener count and so on. If multiple profiles are defined, NVDA will start with the first configuration map (normal profile), and the active profile is denoted by splconfig.SPLConfig map (more on profiles in a moment).
After you are done using Studio, close Studio so settings can be saved to disk. This involves saving individual profiles, copying global settings to the normal profile and saving the normal profile to disk.
The Studio Add-on Settings Dialog
Studio app module allows you to configure various settings in two ways: via a shortcut key (discussed in an article on configuring basic settings) or via the settings dialog. When you use a shortcut key to change settings, NVDA will look up the value for the setting, change it, announce the new setting and store the newly changed value in the settings map.
Alternatively, you can configure settings via the add-on settings dialog (Control+NVDA+0). As it is a settings dialog (powered by gui.SettingsDialog), it will look just like any NVDA preferences dialog. For some advanced options, this dialog is the only gateway to access them (covered below).
The add-on settings dialog (splconfig.SPLConfigDialog) contains following options:
* Broadcast profile controls (add-on 6.0 and later): inspired by NVDA screen reader's configuration profiles dialog, this group of controls shows a list of profiles loaded and buttons to create a brand new profile or a copy of an existing profile, rename and delete profiles. It also contains a button (really a toggle button) that tells NVDA to switch to the selected profile upon request (more on this in a second).
* Global settings: these are settings not affected by profiles. These include status announcements, column announcement order, announcing listener count, toggling Track Dial, library scan options and so on.
* Profile-specific settings: Currently alarm settings are profile-specific. These are end of track alarm and the option to use this alarm, song ramp (intro) time and the setting to use this alarm, and microphone alarm. In case of microphone alarm, Studio stores this value as an integer in the map while it is shown as a string in the add-on settings dialog. For other alarm settings, it is a spin control (wx.SpinCtrl; use up or down arrow keys to change them).
* Reset settings: NVDA will ask if you wish to reset settings in the currently active profile back to factory defaults. This is done by using a function in splconfig module (splconfig.resetConfig) that will set current profile values to defaults (a default configuration map is included for this purpose; this map uses a configuration specification (confspec, part of defining validation routine via validator module (a close friend of ConfigObj), and this confspec is defined in the splconfig module).
When you press Control+NVDA+0 from Studio to open this dialog, the following will happen:
1. Just like alarm dialogs (see previous articles), NVDA will make sure no other dialogs are opened.
2. It'll then call a function in splconfig module, which in turn will prepare the dialog to be shown.
3. The preparation routine (SettingsDialog.makeSettings) will populate the dialog with controls and options associated with each control, with current options coming from configuration values stored in the active profile.
4. Once the dialog is ready, it'll pop up and you'll land on the status message checkbox or list of active profiles depending on the add-on version (former is 5.x, latter is 6.0). You can then use typical dialog navigation commands to navigate through various options.
After configuring some settings, click OK. NVDA will then locate the selected profile and tell SPLConfig to use this profile, then store options from the settings dialog into the configuration map. There is one showstopper, however: if you enter a wrong value into the microphone alarm, NVDA will tell you to enter something else and will return you to microphone alarm field.
In case you discard new settings (clicking Cancel), NVDA will check to see if an instant switch profile is defined, and if so, it'll perform various operations depending on whether the instant profile has been renamed or deleted.
All about broadcast profiles
In Studio app module world, a broadcast profile (usually shortened to profile) is a group of settings to be used in a show. This is achieved by using a configuration profile list (splconfig.SPLConfigPool) for storing these profiles, and one of them is used at any given time (by default, the first profile).
There are two ways of creating a profile: brand new or as a copy. Both uses the same dialog (splconfig.NewProfileDialog), with the difference being the base profile in use. For a brand new profile, settings from the normal profile will be used (minus profile-specific settings, which are set to default values), and for a copy, the new profile will contain all settings from the base profile. In both cases, a memory resident profile will be created and initialized just like other profiles (splconfig.unlockConfig, taking the name of the new profile as a parameter); this was done to reduce unnecessary disk writes. Also, new/copy profile dialog (and other dialogs invoked from the main configuration dialog) will disable the main settings dialog.
In case the selected profile is deleted, the profile will be removed from the profiles list, the configuration file associated with the profile will be deleted (if it exists) and normal profile will be set as the active profile. In case of a rename operation, it'll look for a profile with the old name and change some properties to reflect name change. There is an important step the app module will performed if an instant switch profile is renamed or deleted (if renamed, the instant profile variable will hold the new name, and if deleted, instant profile value will be None).
How does instant profile switching work
An instant switch profile is a profile to be switched to if told by the user. This is used before you connect to a streaming server to load settings appropriate for a show (as of time of this writing, only one can be selected as an instant switch profile; to define this profile, select a profile to be used as a show, then go to profile switching button and select it).
Once a profile is defined as an instant switch profile (via add-on settings dialog), invoke SPL Assistant layer and press F12. NVDA will call splconfig.instantProfileSwitch, which will perform the following:
1. Performs some checks, including:
* Checks if an instant switch profile is defined.
* If a profile is defined, it'll make sure you are not using the instant switch profile yet.
2. Saves the index of the active profile.
3. Locates the name of the instant switch profile and the profile associated with it and switches to the switch profile (reassigns SPLConfig to use the switch profile).
4. If told to return to the previously active profile, it'll tell SPLConfig to use the previously active profile (the index for the previously active profile is located and is used to pull the profile with the given index from the config pool).
Conclusion
Configuration management, if used right, could open up new possibilities and allow you to tailor the program to your liking. Studio add-on for NVDA is no exception to this idea: the configuration management routines discussed above allows you to configure how NVDA acts while using Studio, create profiles to be used in a show and so forth. In essence, all the features we talked about so far (library scan, Track Dial and so on) cannot become personal unless ability to configure settings is in place, and with broadcast profiles, the fun is just beginning.
This article concludes a detailed tour of Studio app module internals. The rest of this series will focus on SPL Studio Utilities global plugin, encoder support and a few thoughts on how the add-on is developed, starting with a tour of SPL Controller layer commands.
References:
1. Configparser (Python documentation, Python Software Foundation): https://docs.python.org/2/library/configparser.html
2. ConfigObj documentation: http://www.voidspace.org.uk/python/configobj.html
3. Validate module documentation: http://www.voidspace.org.uk/python/validate.html
4. Spin control (wx.SpinCtrl) documentation (WXPython): http://wxpython.org/Phoenix/docs/html/SpinCtrl.html
We have arrived at our last station stop for Studio app module internals: configuration management. This facility allows a broadcaster to configure various options such as alarms, column announcement order and so on, as well as package settings for a show as a broadcast profile to be invoked during the show. Along the way you'll learn how NVDA screen reader stores various settings, what happens if something goes wrong and get into internals of how broadcast profiles work.
ConfigObj: NVDA's configuration manager assistant
NVDA uses ConfigObj library to manage settings. This Python module, inspired by Python's own Config Parser, allows developers to store settings in a text file, read and interpret settings and validate options against default configuration options.
NVDA comes with a collection of default options. They live in source/config/__init__ and are used for various things, including presenting preferences, validating user configuration and so on. The config management module also includes facilities to handle profiles (a package of user settings to be used in an app, during say all or reserved for manual activation).
NVDA configuration profiles: multiple configuration files, one online database
A number of users asked NV Access if it would be possible to have profiles where certain settings can take effect while one is using apps or during say all. NV Access listened and introduced configuration profiles in late 2013. As of August 2015, one can create a manual or an automated (triggered) profile, with the latter further divided into say all profile and app-specific one.
Configuration profiles involve a few support routines and a careful coordination between configuration files. In essence, each configuration profile (stored in profiles folder in user configuration folder) is a snapshot of differences between the profile and the main user configuration file (named nvda.ini). When a profile becomes active, NVDA will load the profile file associated with the given profile and modify user settings according to values stored in the newly activated profile, and wwill record the name of the profile file to remind itself as to which profile is active (the default user configuration profile is named "normal configuration" with the file name of nvda.ini).
What if settings had errors? As part of the startup routine (portions of main function (source/core.py) prior to entering the main loop), NVDA will display a configuration error dialog if it detects serious issues with configuration values (in reality, ConfigObj notifies NVDA of this problem). You'll see this is also implemented in the Studio app module to deal with add-on configuration issues.
All about Studio add-on Configuration Manager
Until a few months ago, Studio app module handled all add-on configuration routines. With the advent of add-on 5.0 which introduced add-on settings dialog, configuration management routines were split into a dedicated Configuration Manager (splstudio.splconfig). The new module takes care of configuration routines, including validating the user configuration, presenting add-on settings dialog and other dialogs inside it, handling broadcast profiles and more. We'll cover each routine in this article.
How settings are loaded, used and saved
As mentioned in an article on life of the Studio app module, one of the things the app module does is load the add-on configuration database by calling splconfig.initConfig function. The job of this function is to load the add-on configuration map from various places (for add-on 5.x, it will be the main configuration map only, while 6.0 also searches appModules/profiles folder to load broadcast profiles). The format of the configuration file is that of a typical ini file, and as far as NVDA is concerned, it is a dictionary.
When the configuration database is ready, Studio app module will then use values stored in this settings dictionary to perform various tasks, including microphone alarm, announcing listener count and so on. If multiple profiles are defined, NVDA will start with the first configuration map (normal profile), and the active profile is denoted by splconfig.SPLConfig map (more on profiles in a moment).
After you are done using Studio, close Studio so settings can be saved to disk. This involves saving individual profiles, copying global settings to the normal profile and saving the normal profile to disk.
The Studio Add-on Settings Dialog
Studio app module allows you to configure various settings in two ways: via a shortcut key (discussed in an article on configuring basic settings) or via the settings dialog. When you use a shortcut key to change settings, NVDA will look up the value for the setting, change it, announce the new setting and store the newly changed value in the settings map.
Alternatively, you can configure settings via the add-on settings dialog (Control+NVDA+0). As it is a settings dialog (powered by gui.SettingsDialog), it will look just like any NVDA preferences dialog. For some advanced options, this dialog is the only gateway to access them (covered below).
The add-on settings dialog (splconfig.SPLConfigDialog) contains following options:
* Broadcast profile controls (add-on 6.0 and later): inspired by NVDA screen reader's configuration profiles dialog, this group of controls shows a list of profiles loaded and buttons to create a brand new profile or a copy of an existing profile, rename and delete profiles. It also contains a button (really a toggle button) that tells NVDA to switch to the selected profile upon request (more on this in a second).
* Global settings: these are settings not affected by profiles. These include status announcements, column announcement order, announcing listener count, toggling Track Dial, library scan options and so on.
* Profile-specific settings: Currently alarm settings are profile-specific. These are end of track alarm and the option to use this alarm, song ramp (intro) time and the setting to use this alarm, and microphone alarm. In case of microphone alarm, Studio stores this value as an integer in the map while it is shown as a string in the add-on settings dialog. For other alarm settings, it is a spin control (wx.SpinCtrl; use up or down arrow keys to change them).
* Reset settings: NVDA will ask if you wish to reset settings in the currently active profile back to factory defaults. This is done by using a function in splconfig module (splconfig.resetConfig) that will set current profile values to defaults (a default configuration map is included for this purpose; this map uses a configuration specification (confspec, part of defining validation routine via validator module (a close friend of ConfigObj), and this confspec is defined in the splconfig module).
When you press Control+NVDA+0 from Studio to open this dialog, the following will happen:
1. Just like alarm dialogs (see previous articles), NVDA will make sure no other dialogs are opened.
2. It'll then call a function in splconfig module, which in turn will prepare the dialog to be shown.
3. The preparation routine (SettingsDialog.makeSettings) will populate the dialog with controls and options associated with each control, with current options coming from configuration values stored in the active profile.
4. Once the dialog is ready, it'll pop up and you'll land on the status message checkbox or list of active profiles depending on the add-on version (former is 5.x, latter is 6.0). You can then use typical dialog navigation commands to navigate through various options.
After configuring some settings, click OK. NVDA will then locate the selected profile and tell SPLConfig to use this profile, then store options from the settings dialog into the configuration map. There is one showstopper, however: if you enter a wrong value into the microphone alarm, NVDA will tell you to enter something else and will return you to microphone alarm field.
In case you discard new settings (clicking Cancel), NVDA will check to see if an instant switch profile is defined, and if so, it'll perform various operations depending on whether the instant profile has been renamed or deleted.
All about broadcast profiles
In Studio app module world, a broadcast profile (usually shortened to profile) is a group of settings to be used in a show. This is achieved by using a configuration profile list (splconfig.SPLConfigPool) for storing these profiles, and one of them is used at any given time (by default, the first profile).
There are two ways of creating a profile: brand new or as a copy. Both uses the same dialog (splconfig.NewProfileDialog), with the difference being the base profile in use. For a brand new profile, settings from the normal profile will be used (minus profile-specific settings, which are set to default values), and for a copy, the new profile will contain all settings from the base profile. In both cases, a memory resident profile will be created and initialized just like other profiles (splconfig.unlockConfig, taking the name of the new profile as a parameter); this was done to reduce unnecessary disk writes. Also, new/copy profile dialog (and other dialogs invoked from the main configuration dialog) will disable the main settings dialog.
In case the selected profile is deleted, the profile will be removed from the profiles list, the configuration file associated with the profile will be deleted (if it exists) and normal profile will be set as the active profile. In case of a rename operation, it'll look for a profile with the old name and change some properties to reflect name change. There is an important step the app module will performed if an instant switch profile is renamed or deleted (if renamed, the instant profile variable will hold the new name, and if deleted, instant profile value will be None).
How does instant profile switching work
An instant switch profile is a profile to be switched to if told by the user. This is used before you connect to a streaming server to load settings appropriate for a show (as of time of this writing, only one can be selected as an instant switch profile; to define this profile, select a profile to be used as a show, then go to profile switching button and select it).
Once a profile is defined as an instant switch profile (via add-on settings dialog), invoke SPL Assistant layer and press F12. NVDA will call splconfig.instantProfileSwitch, which will perform the following:
1. Performs some checks, including:
* Checks if an instant switch profile is defined.
* If a profile is defined, it'll make sure you are not using the instant switch profile yet.
2. Saves the index of the active profile.
3. Locates the name of the instant switch profile and the profile associated with it and switches to the switch profile (reassigns SPLConfig to use the switch profile).
4. If told to return to the previously active profile, it'll tell SPLConfig to use the previously active profile (the index for the previously active profile is located and is used to pull the profile with the given index from the config pool).
Conclusion
Configuration management, if used right, could open up new possibilities and allow you to tailor the program to your liking. Studio add-on for NVDA is no exception to this idea: the configuration management routines discussed above allows you to configure how NVDA acts while using Studio, create profiles to be used in a show and so forth. In essence, all the features we talked about so far (library scan, Track Dial and so on) cannot become personal unless ability to configure settings is in place, and with broadcast profiles, the fun is just beginning.
This article concludes a detailed tour of Studio app module internals. The rest of this series will focus on SPL Studio Utilities global plugin, encoder support and a few thoughts on how the add-on is developed, starting with a tour of SPL Controller layer commands.
References:
1. Configparser (Python documentation, Python Software Foundation): https://docs.python.org/2/library/configparser.html
2. ConfigObj documentation: http://www.voidspace.org.uk/python/configobj.html
3. Validate module documentation: http://www.voidspace.org.uk/python/validate.html
4. Spin control (wx.SpinCtrl) documentation (WXPython): http://wxpython.org/Phoenix/docs/html/SpinCtrl.html
StationPlaylist Add-on Internals: All about SPL Assistant layer commands
Hi,
You may recall visiting two layer command sets in a previous article: SPL Controller and SPL Assistant, the former used to perform Studio functions from any program and the latter for status announcements. I mentioned throughout this series that we'll tour these layer sets, and we'll start with SPL Assistant layer.
Talk about layer commands
One of the common features of Studio scripts for JAWS, Window-Eyes and NVDA is extensive use of layer commands. This was popularized by JAWS and its Studio layer (grave key). Some of the benefits of this approach include saving keyboard commands, reminding users as to commands available in Studio and so on.
Birth of SPL Assistant layer
As mentioned previously, since version 1.0 in January 2014, Studio add-on comes with two layer commands to represent the global plugin and the studio app module. In case of Studio app module and its layer set (SPL Assistant), I borrowed some commands from both JAWS and Window-Eyes scripts with some notable differences, namely some commands and how things were announced.
When I sat down to design this layer set, I felt it would be helpful for broadcasters if most of the Assistant layer commands borrowed from Studio command assignments. For example, a broadcaster will press M to toggle microphone on and off, and in SPL Assistant layer, pressing M announces microphone status. Another example was Cart Edit Mode - pressing Control+T in Studio will toggle this, and pressing SPL Assistant, T will announce whether this mode is on or off (the reason for assigning T for Cart Edit Mode status will be discussed later).
Originally, one could invoke SPL Assistant layer by pressing Control+NVDA+grave key from within Studio. However, some NVDA translators told me that this key combination is used for NVDA screen reader commands in their language. Thus, in add-on 2.0 (late spring 2014), I decided to remove this command, which means in order for you (broadcasters) to invoke SPL Assistant layer, you need to go to Input Gestures dialog while focused in Studio, expand StationPlaylist category and look for the Assistant entry (I personally use Control+NvDA+grave, and in recent add-on development builds, I told Studio add-on to let SPL Controller layer command (discussed in a future article) to invoke Assistant layer).
Categorizing SPL Assistant commands
Once you invoke SPL Assistant layer (a beep will be heard), you can perform one of the following operations:
* Status announcements (automation, microphone, etc.).
* Tools (library scan, track time analysis and so on).
* Configuration (switching broadcast profiles).
* Ask for help (opening SPL Assistant help dialog or the online user guide).
For the first two categories, they can be divided further into commands which uses studio API (via statusAPI function discussed in a previous article) and those relying on object navigation (multiple components are involved and is sensitive to user interface changes). We'll go through each of these categories in turn.
SPL Assistant 1: status announcements
These commands allow you to obtain various status information such as title and duration of the next track, cart edit mode status and so on. These can be divided into those which uses object navigation (old style) and Studio API (new style) commands.
The following commands (sorted alphabetically) utilize Studio API to perform needed functions:
* D: Remaining time for the opened playlist.
* H: Duration of tracks in the selected hour.
* P: Playback status.
Revisiting the past: object navigation
Before the new style routines were written, all commands used object navigation. Typically, the command will use a helper function and an object map to locate the needed object and will announce what you are looking for (typically obj.name or an error message). The process was as follows:
1. The Studio app module contains a map of indecies where the object lives in relation to the foreground window. For example, for a given object, if index was 0, NVDA nows that the object is the first child of the foreground object. Technically, it is a dictionary of lists, with each list item (indecies) corresponding to the version of Studio the add-on supports.
2. To fetch needed objects and to record the command type, a number of constants are defined in the app module (all integers, denoting what you wish to hear). These constants serve as keys to the object index map.
3. After entering SPL Assistant layer and once you press one of the commands below, NVDA will do the following:
A. Each command will obtain the object in question by calling object fetcher (status function) with the announcement type as the parameter (status(SPLConstant; for example, for cart edit mode, the signature is self.status(self.SPLPlayStatus), with the constant denoting a status bar).
B. The object fetcher (status function) will first consult an object cache (part of the Studio app module) hoping that the needed object is ready for use (for performance reasons).
C. If the object was not cached, the fetcher will first write down the foreground window, then use the directions specified in the object index map (the constant passed into the status function is the key to this map and different values are returned based on Studio version) to locate, cache and return the object in question (in that order).
D. Back at the routine for the command, it is up to the routine as to what to do with it (sometimes, the actual object is a child of the just returned object).
The commands which utilizes object navigation steps above include:
* A. Automation.
* C: Title of the currently playing track.
* Shift+H: Duration of selected tracks in the current hour slot.
* I: Listener count (I have tried using Studio API to obtain this information, but after experimenting with it, object navigation routine was more stable).
* L: Line in.
* M: Microphone.
* N: Title and duration for the next track.
* Shift+P: Track pitch.
* R: Record to file.
* S: Track scheduled for.
* T: Cart Edit Mode (I assigned T to this command for efficiency reasons).
* U: Studio up time.
* W: Weather and temperature (if configured).
* Y: Playlist modification.
For example, if you press A to obtain automation status from Studio 5.10:
1. Invoke SPL Assistant, then press A.
2. The status function (object fetcher) is called, taking the status bar constant (SPLPlayStatus, which is 0) as the key to the index map.
3. Object fetcher will see if the status bar object (cache dictionary with the key of 0) has been cached. For this example, it isn't.
4. Seeing that the status bar isn't cached, object fetcher will now look at the index map and will decide which column to read (for this example, it is column 2 (index 1)). The column records the child position of the status bar relative to the foreground window (in our case, index is 6 or the seventh child).
5. Once the child object position index is obtained, object fetcher will locate the actual object and cache it (self._cachedStatusObjs[infoIndex] = fg.children[statusObj]), then returns the object to the automation announcement routine.
6. Back at the script routine, NVDA will be reminded that it needs to look at one of the object's children (status bars can contain child objects if exposed by accessibility API's), then will announce one of it's contents (second child object, which records automation status).
SPL Assistant 2: tools
These are miscellaneous commands in SPL Assistant, and all of them (except one) uses Studio API:
* Shift+R: Library scan. This is a convenience function to start library scan in the background, useful if you have added new tracks from a number of folders via Studio Options dialog. Consult a previous article on library scan for details on library scan internals.
* F9: Marks the current position of the playlist as start of track time analysis (more on this feature below).
* F10: Performs track time analysis (add-on 6.0).
Track time analysis: Duration of "selected" tracks
A few months ago, during a Skype chat with a number of add-on users, someone suggested a feature where NvDA will tell you how long it'll take to play selected tracks. Since I was familiar with this concept from JAWS scripts, I decided to work on it as part of add-on 6.0.
The resulting routine (which is available if you are focused on the main playlist viewer with the playlist loaded) is as follows:
1. Move to the position in a playlist to mark as start of track time analysis.
2. Enter SPL assistant, then press F9.
3. Move to another track in the playlist, open SPL Assistant then press F10. NVDA will then:
A. Determine analysis range. For most cases, it'll be top to bottom analysis, but in some cases, it could be reverse (bottom to top). Also, a variable to hold total duration will be prepared.
B. For each track in the analysis range, NvDA will obtain file name and track duration via Studio API. Once the track duration is received, it is then added to the total duration variable.
C. Once time analysis (calculating total duration) is done, NVDA will announce number of tracks selected and the total duration using mm:ss format.
If you are a seasoned NVDA user, you may have noticed a familiar pattern: the command to set a marker to copy review cursor text is NVDA+F9, and you would move to a different location and press NvDA+F10 to copy the selected text to the clipboard. Replacing the NvDA modifier key with SPL Assistant produces the commands above: F9 to mark current position for time analysis, and F10 to perform the actual calculation. I intentionally chose these two function keys to provide consistent experience and to reenforce concepts used in NvDA screen reader: review cursor.
SPL Assistant 3: configuration
There is another function key assigned to SPL Assistant: pressing F12 will switch to an instant switch profile (if defined). We'll come back to what is meant by "instant switch profile" and the mechanics of it (and internals of SPL Assistant, F12) in the next article.
SPL Assistant 4: getting help
I believe that a product isn't complete without a good quality documentation. For this reason, SPL Assistant provides two commands to help you use the layer commands or the add-on itself. They are:
* F1: Displays a dialog presenting a list of SPL Assistant layer commands.
* Shift+F1: Opens the online user guide (os.startfile).
A surprise: some Assistant layer commands can be invoked without entering the layer first
There are times when a broadcaster will need to obtain certain information quickly. So the question becomes, "is there a way to announce something without first invoking Assistant layer?" Yes, you can assigg a custom command for the following Assistant commands:
* Name of the next track.
* Name of the current track.
* Weather and temperature.
* Track time analysis marker.
* Track time analysis.
For these routines, an extra step is performed to make sure that SPL Assistant flag is turned off automatically after the shortcut for these routines are pressed. Without this step, you might end up with a situation like the following:
1. You invoke Assistant layer.
2. You then press the shortcut key (not the layer counterpart) for the layer command you wish to use.
3. You press another key which may announce something else, or you hear the same thing twice if you do press the layer command counterpart to the command you have pressed. In effect, you have invoked two layer commands in one sitting (the purpose of the layer set is to let you hear one announcement at a time).
Conclusion
By now you should have a better understanding of how SPL Assistant layer commands work. You also learned that some commands do not require you to invoke the Assistant layer, and toured the necessary infrastructure to support certain layer commands. We'll visit the Controller layer commands in a future article. Our next station stop is an overview of add-on configuration management internals, including how NVDA can remember various settings, present options in add-on settings dialog and an introduction to broadcast profiles.
References:
1. Cache (Wikipedia): https://en.wikipedia.org/wiki/Cache_(computing)
2. Os (Python documentation, Python Software Foundation): https://docs.python.org/2/library/os.html
You may recall visiting two layer command sets in a previous article: SPL Controller and SPL Assistant, the former used to perform Studio functions from any program and the latter for status announcements. I mentioned throughout this series that we'll tour these layer sets, and we'll start with SPL Assistant layer.
Talk about layer commands
One of the common features of Studio scripts for JAWS, Window-Eyes and NVDA is extensive use of layer commands. This was popularized by JAWS and its Studio layer (grave key). Some of the benefits of this approach include saving keyboard commands, reminding users as to commands available in Studio and so on.
Birth of SPL Assistant layer
As mentioned previously, since version 1.0 in January 2014, Studio add-on comes with two layer commands to represent the global plugin and the studio app module. In case of Studio app module and its layer set (SPL Assistant), I borrowed some commands from both JAWS and Window-Eyes scripts with some notable differences, namely some commands and how things were announced.
When I sat down to design this layer set, I felt it would be helpful for broadcasters if most of the Assistant layer commands borrowed from Studio command assignments. For example, a broadcaster will press M to toggle microphone on and off, and in SPL Assistant layer, pressing M announces microphone status. Another example was Cart Edit Mode - pressing Control+T in Studio will toggle this, and pressing SPL Assistant, T will announce whether this mode is on or off (the reason for assigning T for Cart Edit Mode status will be discussed later).
Originally, one could invoke SPL Assistant layer by pressing Control+NVDA+grave key from within Studio. However, some NVDA translators told me that this key combination is used for NVDA screen reader commands in their language. Thus, in add-on 2.0 (late spring 2014), I decided to remove this command, which means in order for you (broadcasters) to invoke SPL Assistant layer, you need to go to Input Gestures dialog while focused in Studio, expand StationPlaylist category and look for the Assistant entry (I personally use Control+NvDA+grave, and in recent add-on development builds, I told Studio add-on to let SPL Controller layer command (discussed in a future article) to invoke Assistant layer).
Categorizing SPL Assistant commands
Once you invoke SPL Assistant layer (a beep will be heard), you can perform one of the following operations:
* Status announcements (automation, microphone, etc.).
* Tools (library scan, track time analysis and so on).
* Configuration (switching broadcast profiles).
* Ask for help (opening SPL Assistant help dialog or the online user guide).
For the first two categories, they can be divided further into commands which uses studio API (via statusAPI function discussed in a previous article) and those relying on object navigation (multiple components are involved and is sensitive to user interface changes). We'll go through each of these categories in turn.
SPL Assistant 1: status announcements
These commands allow you to obtain various status information such as title and duration of the next track, cart edit mode status and so on. These can be divided into those which uses object navigation (old style) and Studio API (new style) commands.
The following commands (sorted alphabetically) utilize Studio API to perform needed functions:
* D: Remaining time for the opened playlist.
* H: Duration of tracks in the selected hour.
* P: Playback status.
Revisiting the past: object navigation
Before the new style routines were written, all commands used object navigation. Typically, the command will use a helper function and an object map to locate the needed object and will announce what you are looking for (typically obj.name or an error message). The process was as follows:
1. The Studio app module contains a map of indecies where the object lives in relation to the foreground window. For example, for a given object, if index was 0, NVDA nows that the object is the first child of the foreground object. Technically, it is a dictionary of lists, with each list item (indecies) corresponding to the version of Studio the add-on supports.
2. To fetch needed objects and to record the command type, a number of constants are defined in the app module (all integers, denoting what you wish to hear). These constants serve as keys to the object index map.
3. After entering SPL Assistant layer and once you press one of the commands below, NVDA will do the following:
A. Each command will obtain the object in question by calling object fetcher (status function) with the announcement type as the parameter (status(SPLConstant; for example, for cart edit mode, the signature is self.status(self.SPLPlayStatus), with the constant denoting a status bar).
B. The object fetcher (status function) will first consult an object cache (part of the Studio app module) hoping that the needed object is ready for use (for performance reasons).
C. If the object was not cached, the fetcher will first write down the foreground window, then use the directions specified in the object index map (the constant passed into the status function is the key to this map and different values are returned based on Studio version) to locate, cache and return the object in question (in that order).
D. Back at the routine for the command, it is up to the routine as to what to do with it (sometimes, the actual object is a child of the just returned object).
The commands which utilizes object navigation steps above include:
* A. Automation.
* C: Title of the currently playing track.
* Shift+H: Duration of selected tracks in the current hour slot.
* I: Listener count (I have tried using Studio API to obtain this information, but after experimenting with it, object navigation routine was more stable).
* L: Line in.
* M: Microphone.
* N: Title and duration for the next track.
* Shift+P: Track pitch.
* R: Record to file.
* S: Track scheduled for.
* T: Cart Edit Mode (I assigned T to this command for efficiency reasons).
* U: Studio up time.
* W: Weather and temperature (if configured).
* Y: Playlist modification.
For example, if you press A to obtain automation status from Studio 5.10:
1. Invoke SPL Assistant, then press A.
2. The status function (object fetcher) is called, taking the status bar constant (SPLPlayStatus, which is 0) as the key to the index map.
3. Object fetcher will see if the status bar object (cache dictionary with the key of 0) has been cached. For this example, it isn't.
4. Seeing that the status bar isn't cached, object fetcher will now look at the index map and will decide which column to read (for this example, it is column 2 (index 1)). The column records the child position of the status bar relative to the foreground window (in our case, index is 6 or the seventh child).
5. Once the child object position index is obtained, object fetcher will locate the actual object and cache it (self._cachedStatusObjs[infoIndex] = fg.children[statusObj]), then returns the object to the automation announcement routine.
6. Back at the script routine, NVDA will be reminded that it needs to look at one of the object's children (status bars can contain child objects if exposed by accessibility API's), then will announce one of it's contents (second child object, which records automation status).
SPL Assistant 2: tools
These are miscellaneous commands in SPL Assistant, and all of them (except one) uses Studio API:
* Shift+R: Library scan. This is a convenience function to start library scan in the background, useful if you have added new tracks from a number of folders via Studio Options dialog. Consult a previous article on library scan for details on library scan internals.
* F9: Marks the current position of the playlist as start of track time analysis (more on this feature below).
* F10: Performs track time analysis (add-on 6.0).
Track time analysis: Duration of "selected" tracks
A few months ago, during a Skype chat with a number of add-on users, someone suggested a feature where NvDA will tell you how long it'll take to play selected tracks. Since I was familiar with this concept from JAWS scripts, I decided to work on it as part of add-on 6.0.
The resulting routine (which is available if you are focused on the main playlist viewer with the playlist loaded) is as follows:
1. Move to the position in a playlist to mark as start of track time analysis.
2. Enter SPL assistant, then press F9.
3. Move to another track in the playlist, open SPL Assistant then press F10. NVDA will then:
A. Determine analysis range. For most cases, it'll be top to bottom analysis, but in some cases, it could be reverse (bottom to top). Also, a variable to hold total duration will be prepared.
B. For each track in the analysis range, NvDA will obtain file name and track duration via Studio API. Once the track duration is received, it is then added to the total duration variable.
C. Once time analysis (calculating total duration) is done, NVDA will announce number of tracks selected and the total duration using mm:ss format.
If you are a seasoned NVDA user, you may have noticed a familiar pattern: the command to set a marker to copy review cursor text is NVDA+F9, and you would move to a different location and press NvDA+F10 to copy the selected text to the clipboard. Replacing the NvDA modifier key with SPL Assistant produces the commands above: F9 to mark current position for time analysis, and F10 to perform the actual calculation. I intentionally chose these two function keys to provide consistent experience and to reenforce concepts used in NvDA screen reader: review cursor.
SPL Assistant 3: configuration
There is another function key assigned to SPL Assistant: pressing F12 will switch to an instant switch profile (if defined). We'll come back to what is meant by "instant switch profile" and the mechanics of it (and internals of SPL Assistant, F12) in the next article.
SPL Assistant 4: getting help
I believe that a product isn't complete without a good quality documentation. For this reason, SPL Assistant provides two commands to help you use the layer commands or the add-on itself. They are:
* F1: Displays a dialog presenting a list of SPL Assistant layer commands.
* Shift+F1: Opens the online user guide (os.startfile).
A surprise: some Assistant layer commands can be invoked without entering the layer first
There are times when a broadcaster will need to obtain certain information quickly. So the question becomes, "is there a way to announce something without first invoking Assistant layer?" Yes, you can assigg a custom command for the following Assistant commands:
* Name of the next track.
* Name of the current track.
* Weather and temperature.
* Track time analysis marker.
* Track time analysis.
For these routines, an extra step is performed to make sure that SPL Assistant flag is turned off automatically after the shortcut for these routines are pressed. Without this step, you might end up with a situation like the following:
1. You invoke Assistant layer.
2. You then press the shortcut key (not the layer counterpart) for the layer command you wish to use.
3. You press another key which may announce something else, or you hear the same thing twice if you do press the layer command counterpart to the command you have pressed. In effect, you have invoked two layer commands in one sitting (the purpose of the layer set is to let you hear one announcement at a time).
Conclusion
By now you should have a better understanding of how SPL Assistant layer commands work. You also learned that some commands do not require you to invoke the Assistant layer, and toured the necessary infrastructure to support certain layer commands. We'll visit the Controller layer commands in a future article. Our next station stop is an overview of add-on configuration management internals, including how NVDA can remember various settings, present options in add-on settings dialog and an introduction to broadcast profiles.
References:
1. Cache (Wikipedia): https://en.wikipedia.org/wiki/Cache_(computing)
2. Os (Python documentation, Python Software Foundation): https://docs.python.org/2/library/os.html
Saturday, August 22, 2015
StationPlaylist Add-on Internals: The magic behind Cart Explorer
IMPORTANT: The following article superseeds a previous blog entry (summer 2014) regarding this subject.
Hi,
A live radio broadcast would not be complete without jingles. This can range from station promotions (often called "promos"), advertisements, jingles to convey the mood of a show, segment jingles and more. Many station automation programs, including StationPlaylist Studio includes facilities to manage jingles (sometimes called carts), including defining a cart to be played when cart keys are pressed, announcing the name of the playing cart and saving uer specific carts to a safe location.
For blind broadcasters, one of the things they worry is pressing a wrong jingle key by accident, thus script writers were asked to implement a way for broadcasters to learn which carts are assigned to cart keys. As of time of this post, all three screen readers (JAWS for Windows (script author: Brian Hartgen), Window-Eyes (script author: Jeff Bishop), NVDA (script author: Joseph Lee (I, the author of this article)) includes a feature to learn jingle assignments. As this is a series of articles on internals of an NVDA add-on, I'll give you an overview of what happens when you activate and explore cart assignments (in fact, this section was the most interesting and feedback driven portion of the add-on). Along the way you'll learn where Cart Explorer (NVDA's version of cart learn mode) draws its power and why it is very important.
Carts in StationPlaylist Studio
Studio comes in three editions: Demo (same as Pro but for limited time trial), Standard and Pro. The first user visible difference between Standard and Pro is number of cart assignments: Standard can store 48 jingles, while Pro can work with 96 of them.
To play jingles, a broadcaster would use Cart Edit Mode Control+T), then assign a hotkey to a file. For Studio Standard, you can assign F1 through F12 by themselves or in combination with Control, Shift or Alt. In Demo and Pro, number row can be assigned (1 through 9, 0, hyphen (-) and equals (=) either by themselves or in combination with Control, Shift or Alt, for a grand total of 96 jingles). Once jingles are assigned, they will appear under cart menus (there are four cart menus, one for standalone keys (called main) and one each for Control, Shift and Alt).
Where does Studio store carts?
Studio's "carts" are housed in Studio installation folder. There are four cart files (called banks) in use: a .cart file for each of the cart banks (main, Shift, Control, Alt). During normal business hours, Studio will work with these four banks unless told by a broadcaster to load carts from a different cart bank file.
Cart Explorer: my own Summer of Code
It was a hot day in June 2014 when I sat down to design a way to let broadcasters learn cart assignments. Since I was developing add-on 3.0 back then, I decided that this feature should be a top priority feature to be included in the upcoming release.
When I started writing this feature, the first thing I thought about was its name. I felt "cart learn mode" didn't really convey the picture - after all, I reasoned that broadcasters will use this feature to explore cart assignments. Thus the name "Cart Explorer" was chosen - in effect, when you use this feature, you are browsing jingle assignments in preparation for a show.
Next, I read JAWS scripts documentation to get a glimpse of how Brian has managed to implement cart learn mode. In JAWS scripts, script settings are stored in the user configuration directory (typically this is %systemdrive%\Users\%username%\AppDate\Roaming\Freedom Scientific\JAWS\%JAWSVersion%\Settings\Enu; Brian, please correct me if I'm wrong). A section of this script configuration file is dedicated to carts, and JAWS scripts use a map of key names and cart values to announce cart information while cart learn mode is active.
Based on this information, I started writing an ini file parser, seeing that broadcasters would store cart assignments in a configuration database. This was prone to a number of errors, including wrong cart name format, nonexistent cart key assignment, invalid configuration format and others. I once wrote a blog post (on this blog) explaining how this worked (times have changed, as you'll see very soon).
Then I became curious as to how Studio stores its own cart banks, and naturally, I opened the folder where carts were stored and opened each .cart file in Notepad++ (a very handy text editor). From reading the cart bank format (explained below), I thought it might be useful to write a cart bank file parser. Thus I resumed writing Cart Explorer routines, this time incorporating the cart bank format, not forgetting to handle suttle errors, and this is the routine used in add-on releases up to 5.x (6.0 uses a completely different yet related routine, as you'll see).
While writing the first version of Cart Explorer, I realized that this feature needed some real life testing, so I asked a seasoned blind broadcaster to test this feature. We spent a better part of Independence Day writing, debugging and rewriting this routine until we felt satisfied. In the end, our hard work paid off, as you can see in subsequent paragraphs.
Introducing Cart Explorer version 1
Cart Explorer version 1, shipped as part of add-on 3.0, worked as follows:
1. You press Control+NvDA+3 to activate Cart Explorer. When this happens, NVDA will make sure you are in main playlist viewer, then it will set a flag indicating that Cart Explorer is active.
2. NVDA will then open and parse cart bank files, storing cart assignments in a dictionary of cart keys to cart names. This parser also takes care of some corner cases, including skipping unassigned carts and determining Studio edition in use. Once carts were parsed, NVDA says, "Entering Cart Explorer", and if errors occur, NVDA will inform you that it cannot enter Cart Explorer (this happens if the cart bank file doesn't exist).
3. While using Cart Explorer, if you press a cart key, NVDA will look up the name of the key in the carts dictionary, and announce the cart name associated with it (if found, otherwise, NVDA says, "cart unassigned"). If you are using Standard edition and press number row keys, NVDA will warn you that cart commands are unavailable.
4. It so happens that some people will activate Cart Edit Mode to modify cart assignments while in the middle of exploring carts. If this happens, NVDA will remind you (via doExtraAction function used by name change event) that Cart Explorer is active, and when Cart Edit Mode is turned off, NVDA will ask you to reenter Cart Explorer (this was done to parse newly updated cart bank files).
5. You press Control+NVDA+3, and NVDA will clear carts dictionary, thereby leaving Cart Explorer.
But there was a major concern with this approach: what if a future version of Studio uses a different cart bank format? Thus, I revisited cart bank files again in July 2015, and this time, I noticed a familiar structure: comma-separated values. To test my hypothesis, I opened .cart files in Excel, and voila, it presented itself just like any CSV file. Thus I worked on modifying cart parsing routine, this time using Python's CSV module to parse "cart" files (cart bank files are really CSV files in disguise). This new routine (described below) will make its appearance as part of add-on 6.0.
The magic behind Cart Explorer: handling CSV files
Since Python comes with a library to handle CSV files and since cart banks are CSV files, I rewrote Cart Explorer routine (a function in splmisc module which returns the carts dictionary) as follows:
1. When entering Cart Explorer, Car Explorer preparation routine (splmisc.cartExplorerInit) will take a snapshot of your user name and Studio edition (Studio's title bar indicates which version is in use). Then it initializes the carts dictionary and stores the Studio edition in use.
2. Next, the preparation function will write down names and paths to cart banks. In case a user other than default user is using Studio, it'll modify the cart file names to match the name likely to be used by Studio to present user-specific cart banks. These cart names form one part of the cart bank path (the other part is the path to the folder where the carts live, obtained by using an environment variable).
3. For each cart bank, NVDA will ask Python to parse the cart bank as a CSV file (csv.reader; when finished, it returns a list of lists, with each list representing one row of a CSV table).
4. Once the csv version of the selected cart bank is ready, the row containing cart keys and cart names, together with the cart bank modifier and the carts dictionary are sent to a helper function (part of splmisc module) that will do the following:
A. Depending on Studio edition, this helper will work with only the first half (Standard will only work with function keys, which are the first twelve columns of this row) or the entire row (the rest are number row keys) will be processed.
B. Depending on column position (items in the row list), it will see if function keys or number row keys should be assigned to the selected cart entry. This routine also checks the type of the cart bank (modifiers or none (main)) and modifies the cart key name accordingly.
C. Next, the helper routine will try to locate the name of the jingle assigned to the cart key in question, and if there is one, it'll add the cart key and jingle name pair into the carts dictionary.
5. Back at the cartExplorerInit function, if no erorrs were found while parsing a cart bank, it'll move onto the next one, otherwise it will inform the Studio app module by modifying a flag value in the carts dictionary (stored as an integer, representing number of cart bankks with errors).
6. By now cartExplorerInit is desperate to pass the carts dictionary to someone, and this someone turns out to be the Studio app module - once picked up by the app module, carts dictionary is hired by you to look up cart names for cart keys while you use Cart Explorer (to fire the carts dictionary, simply deactivate Cart Explorer by pressing Control+NvDA+3).
In effect, the routine above (the "magic" behind Cart Explorer) replaced a hand-written cart bank parser and simplified the add-on code (I regret not investigating CSV last year). As far as user experience is concerned, this is same as Cart Explorer 1, with the difference being the parsing routine. With the addition of splmisc.cartExplorerInit, the current version of the splmisc module (miscellaneous services, containing the Track Finder/Column Search combo dialog, column retriever and Cart Explorer preparation tool) was completed.
Conclusion
Cart Explorer has come a long way; from a simple suggestion to the CSV parsing routine above, Cart Explorer has changed to meet the needs of broadcasters using Studio and NVDA. I would like to improve this further in future releases (another suggestion I received was ability to specify cart file names for individual banks, and I'm thinking about implementing this in the near future).
One of the things you may have noticed as you read this article is how I and other developers continue to research better ways of accomplishing something. You also saw a glimpse of how developers and users shape a feature and how much work is involved to bring a feature suggestion to life. These activities (research and feature development collaboration) are just two of the pillars supporting Studio add-on for NVDA, and highlights how design philosophy and product development approach affects future course of product development.
This ends our detailed tour of internals of major features in Studio app module. When we come back, we'll visit our friend from the past: SPL Assistant layer and inner workings of various layer commands.
References:
1. Comma-separated values (Wikipedia): https://en.wikipedia.org/wiki/Comma-separated_values
2. RFC 4180 (Common Format and MIME Type for Comma-Separated Values (CSV) Files), Internet Engineering Task Force: https://tools.ietf.org/html/rfc4180
3. Import or export text (.txt or .csv) files, Microsoft Office Support for Microsoft Excel: https://support.office.com/en-za/article/Import-or-export-text-txt-or-csv-files-5250ac4c-663c-47ce-937b-339e391393ba
4. CSV (Python documentation, Python Software Foundation): https://docs.python.org/2/library/csv.html
Hi,
A live radio broadcast would not be complete without jingles. This can range from station promotions (often called "promos"), advertisements, jingles to convey the mood of a show, segment jingles and more. Many station automation programs, including StationPlaylist Studio includes facilities to manage jingles (sometimes called carts), including defining a cart to be played when cart keys are pressed, announcing the name of the playing cart and saving uer specific carts to a safe location.
For blind broadcasters, one of the things they worry is pressing a wrong jingle key by accident, thus script writers were asked to implement a way for broadcasters to learn which carts are assigned to cart keys. As of time of this post, all three screen readers (JAWS for Windows (script author: Brian Hartgen), Window-Eyes (script author: Jeff Bishop), NVDA (script author: Joseph Lee (I, the author of this article)) includes a feature to learn jingle assignments. As this is a series of articles on internals of an NVDA add-on, I'll give you an overview of what happens when you activate and explore cart assignments (in fact, this section was the most interesting and feedback driven portion of the add-on). Along the way you'll learn where Cart Explorer (NVDA's version of cart learn mode) draws its power and why it is very important.
Carts in StationPlaylist Studio
Studio comes in three editions: Demo (same as Pro but for limited time trial), Standard and Pro. The first user visible difference between Standard and Pro is number of cart assignments: Standard can store 48 jingles, while Pro can work with 96 of them.
To play jingles, a broadcaster would use Cart Edit Mode Control+T), then assign a hotkey to a file. For Studio Standard, you can assign F1 through F12 by themselves or in combination with Control, Shift or Alt. In Demo and Pro, number row can be assigned (1 through 9, 0, hyphen (-) and equals (=) either by themselves or in combination with Control, Shift or Alt, for a grand total of 96 jingles). Once jingles are assigned, they will appear under cart menus (there are four cart menus, one for standalone keys (called main) and one each for Control, Shift and Alt).
Where does Studio store carts?
Studio's "carts" are housed in Studio installation folder. There are four cart files (called banks) in use: a .cart file for each of the cart banks (main, Shift, Control, Alt). During normal business hours, Studio will work with these four banks unless told by a broadcaster to load carts from a different cart bank file.
Cart Explorer: my own Summer of Code
It was a hot day in June 2014 when I sat down to design a way to let broadcasters learn cart assignments. Since I was developing add-on 3.0 back then, I decided that this feature should be a top priority feature to be included in the upcoming release.
When I started writing this feature, the first thing I thought about was its name. I felt "cart learn mode" didn't really convey the picture - after all, I reasoned that broadcasters will use this feature to explore cart assignments. Thus the name "Cart Explorer" was chosen - in effect, when you use this feature, you are browsing jingle assignments in preparation for a show.
Next, I read JAWS scripts documentation to get a glimpse of how Brian has managed to implement cart learn mode. In JAWS scripts, script settings are stored in the user configuration directory (typically this is %systemdrive%\Users\%username%\AppDate\Roaming\Freedom Scientific\JAWS\%JAWSVersion%\Settings\Enu; Brian, please correct me if I'm wrong). A section of this script configuration file is dedicated to carts, and JAWS scripts use a map of key names and cart values to announce cart information while cart learn mode is active.
Based on this information, I started writing an ini file parser, seeing that broadcasters would store cart assignments in a configuration database. This was prone to a number of errors, including wrong cart name format, nonexistent cart key assignment, invalid configuration format and others. I once wrote a blog post (on this blog) explaining how this worked (times have changed, as you'll see very soon).
Then I became curious as to how Studio stores its own cart banks, and naturally, I opened the folder where carts were stored and opened each .cart file in Notepad++ (a very handy text editor). From reading the cart bank format (explained below), I thought it might be useful to write a cart bank file parser. Thus I resumed writing Cart Explorer routines, this time incorporating the cart bank format, not forgetting to handle suttle errors, and this is the routine used in add-on releases up to 5.x (6.0 uses a completely different yet related routine, as you'll see).
While writing the first version of Cart Explorer, I realized that this feature needed some real life testing, so I asked a seasoned blind broadcaster to test this feature. We spent a better part of Independence Day writing, debugging and rewriting this routine until we felt satisfied. In the end, our hard work paid off, as you can see in subsequent paragraphs.
Introducing Cart Explorer version 1
Cart Explorer version 1, shipped as part of add-on 3.0, worked as follows:
1. You press Control+NvDA+3 to activate Cart Explorer. When this happens, NVDA will make sure you are in main playlist viewer, then it will set a flag indicating that Cart Explorer is active.
2. NVDA will then open and parse cart bank files, storing cart assignments in a dictionary of cart keys to cart names. This parser also takes care of some corner cases, including skipping unassigned carts and determining Studio edition in use. Once carts were parsed, NVDA says, "Entering Cart Explorer", and if errors occur, NVDA will inform you that it cannot enter Cart Explorer (this happens if the cart bank file doesn't exist).
3. While using Cart Explorer, if you press a cart key, NVDA will look up the name of the key in the carts dictionary, and announce the cart name associated with it (if found, otherwise, NVDA says, "cart unassigned"). If you are using Standard edition and press number row keys, NVDA will warn you that cart commands are unavailable.
4. It so happens that some people will activate Cart Edit Mode to modify cart assignments while in the middle of exploring carts. If this happens, NVDA will remind you (via doExtraAction function used by name change event) that Cart Explorer is active, and when Cart Edit Mode is turned off, NVDA will ask you to reenter Cart Explorer (this was done to parse newly updated cart bank files).
5. You press Control+NVDA+3, and NVDA will clear carts dictionary, thereby leaving Cart Explorer.
But there was a major concern with this approach: what if a future version of Studio uses a different cart bank format? Thus, I revisited cart bank files again in July 2015, and this time, I noticed a familiar structure: comma-separated values. To test my hypothesis, I opened .cart files in Excel, and voila, it presented itself just like any CSV file. Thus I worked on modifying cart parsing routine, this time using Python's CSV module to parse "cart" files (cart bank files are really CSV files in disguise). This new routine (described below) will make its appearance as part of add-on 6.0.
The magic behind Cart Explorer: handling CSV files
Since Python comes with a library to handle CSV files and since cart banks are CSV files, I rewrote Cart Explorer routine (a function in splmisc module which returns the carts dictionary) as follows:
1. When entering Cart Explorer, Car Explorer preparation routine (splmisc.cartExplorerInit) will take a snapshot of your user name and Studio edition (Studio's title bar indicates which version is in use). Then it initializes the carts dictionary and stores the Studio edition in use.
2. Next, the preparation function will write down names and paths to cart banks. In case a user other than default user is using Studio, it'll modify the cart file names to match the name likely to be used by Studio to present user-specific cart banks. These cart names form one part of the cart bank path (the other part is the path to the folder where the carts live, obtained by using an environment variable).
3. For each cart bank, NVDA will ask Python to parse the cart bank as a CSV file (csv.reader; when finished, it returns a list of lists, with each list representing one row of a CSV table).
4. Once the csv version of the selected cart bank is ready, the row containing cart keys and cart names, together with the cart bank modifier and the carts dictionary are sent to a helper function (part of splmisc module) that will do the following:
A. Depending on Studio edition, this helper will work with only the first half (Standard will only work with function keys, which are the first twelve columns of this row) or the entire row (the rest are number row keys) will be processed.
B. Depending on column position (items in the row list), it will see if function keys or number row keys should be assigned to the selected cart entry. This routine also checks the type of the cart bank (modifiers or none (main)) and modifies the cart key name accordingly.
C. Next, the helper routine will try to locate the name of the jingle assigned to the cart key in question, and if there is one, it'll add the cart key and jingle name pair into the carts dictionary.
5. Back at the cartExplorerInit function, if no erorrs were found while parsing a cart bank, it'll move onto the next one, otherwise it will inform the Studio app module by modifying a flag value in the carts dictionary (stored as an integer, representing number of cart bankks with errors).
6. By now cartExplorerInit is desperate to pass the carts dictionary to someone, and this someone turns out to be the Studio app module - once picked up by the app module, carts dictionary is hired by you to look up cart names for cart keys while you use Cart Explorer (to fire the carts dictionary, simply deactivate Cart Explorer by pressing Control+NvDA+3).
In effect, the routine above (the "magic" behind Cart Explorer) replaced a hand-written cart bank parser and simplified the add-on code (I regret not investigating CSV last year). As far as user experience is concerned, this is same as Cart Explorer 1, with the difference being the parsing routine. With the addition of splmisc.cartExplorerInit, the current version of the splmisc module (miscellaneous services, containing the Track Finder/Column Search combo dialog, column retriever and Cart Explorer preparation tool) was completed.
Conclusion
Cart Explorer has come a long way; from a simple suggestion to the CSV parsing routine above, Cart Explorer has changed to meet the needs of broadcasters using Studio and NVDA. I would like to improve this further in future releases (another suggestion I received was ability to specify cart file names for individual banks, and I'm thinking about implementing this in the near future).
One of the things you may have noticed as you read this article is how I and other developers continue to research better ways of accomplishing something. You also saw a glimpse of how developers and users shape a feature and how much work is involved to bring a feature suggestion to life. These activities (research and feature development collaboration) are just two of the pillars supporting Studio add-on for NVDA, and highlights how design philosophy and product development approach affects future course of product development.
This ends our detailed tour of internals of major features in Studio app module. When we come back, we'll visit our friend from the past: SPL Assistant layer and inner workings of various layer commands.
References:
1. Comma-separated values (Wikipedia): https://en.wikipedia.org/wiki/Comma-separated_values
2. RFC 4180 (Common Format and MIME Type for Comma-Separated Values (CSV) Files), Internet Engineering Task Force: https://tools.ietf.org/html/rfc4180
3. Import or export text (.txt or .csv) files, Microsoft Office Support for Microsoft Excel: https://support.office.com/en-za/article/Import-or-export-text-txt-or-csv-files-5250ac4c-663c-47ce-937b-339e391393ba
4. CSV (Python documentation, Python Software Foundation): https://docs.python.org/2/library/csv.html
StationPlaylist Add-on Internals: Library scan and microphone alarm: threads, threads and more threads
IMPORTANT: The following article on threads, especially a discussion on library scan may change in a future add-on release (I'm experimenting with a new routine for library scan that will use timers instead of a threaded while loop; I'll provide an update via an addendum if timer routine experiment is successful).
Hi,
Of all the work done on Studio add-on, one of them stands out the most: background tasks. I spent many hours and months perfecting this concept, read documentation on this feature and learned a lot through this experience. Today, this work is used in various parts of the Studio app module and beyond, and we'll take a look at two most important results of this work: library scan and microphone alarm.
Brief feature overview
When you are producing a live show, you may forget that your microphone is active. The microphone alarm feature lets NVDA notify you if microphone has been active for a while. This happens even if you are using another program.
Library scan comes in handy when you want to see the progress of a background library scan. Typically, you would initiate library scans from Insert Tracks dialog (press Control+Shift+R). NvDA will then tell you how the scan is going, and if you close Insert Tracks dialog, NVDA will continue to monitor library scans in the background.
But there's more to it than a simple description when it comes to looking at internals of these features. As you'll see, these features use a concept that is gaining traction: running multiple tasks at once, or at least try to emulate it. We'll visit this concept first before returning to our regularly scheduled program of describing the internals of the two features above.
Recent trends in computing: more and more processors in a single computer
A decade ago, people thought a single core CPU was enough to run multiple programs. This involved the processor spending small fraction of a second devoted to each program. Nowadays, it has become common to see desktops, laptops, smartphones and other small devices using at least two cores (termed multi-core; two cores is dubbed "dual core"). As of August 2015, many computers use processors with four cores (dubbed "quad core"), while enthusiasts prefer more cores (the current record holder for desktop computers (as of August 2015) is 8 (octa core), held by Intel Core I7-5960X, a 3 GHz unlocked processor which costs about a thousand dollars; for servers where more cores are used, the current record holder is a package of eight processors (octa processor) called Intel Xeon E7-8895V3, with each processor boasting eighteen cores with base speed of 2.6 GHz).
Despite the fact that many computers come equipped with multi-core processors, not all programs take advantage of this. Python interpreter is one of those programs, and since NVDA is a Python-based screen reader and due to its operational architecture, many of its operations cannot take advantage of multiple processors. Fortunately, Python provides a way to simulate this - run certain tasks in the background, and this is utilized by NVDA and some of its add-ons as you'll see in this article on library scan and microphone alarm.
A gentle introduction to threads: multiple tasks at once
During normal business hours, a program will run from beginning to end with some interuptions (keyboard input, switching to a different part of the program and so on). However, there are times when the program will need to work with many things simultaneously, such as calculating distance between many points, adding multiple numbers at once, comparing many pairs of strings and so on. Fortunately, a mechanism called threads allow a program to do multiple things simultaneously.
A thread is a procedure independent of other tasks. If one thread is busy with something, other threads can work on other tasks. The best analogy is multiple bank tellers in a bank: customers can talk to different tellers, with one teller working on updating customer records for a customer while another customer discusses fraudulent credit card charges with a different teller.
A thread can be involved with parts of a task, devoted to a single task or multiple tasks. For example, an antivirus program could have multiple threads (workers) working independently of each other. One worker can display the overall progress of a scan, while other threads can scan multiple drives at once, with each thread devoted to scanning files and folders on separate drives. In NVDA world, multiple workers are involved to perform various tasks, including making sure NVDA is responsive, handling browse mode in different web browsers and so on.
Threads: a more geeky introduction
A thread (sometimes termed "thread of execution) is an independent path of execution. A single process (app) can have as many threads as it desires (minimum is one for the main thread). Each thread can be asked to perform certain operations with other threads in parallel, which can range from a single, repetative task (part of a function) to being responsible for an entire module or a significant part of the program. In case of antivirus example above, each scanner thread is responsible for scanning an entire drive, with each of them reporting its progress to a manager thread which displays overall progress of a virus scan.
Using threads means each thread can execute on a processor core on a multi-core system. Because of this, many people would want many programs to take advantage of this and finish their jobs faster. However, threads introduce disadvantages, namely many days spent designing careful coordination routines between threads, preventing attempts by multiple threads to change a critical value that a manager thread depends on (called race condition) and so forth.
Python's way of managing threads and the threading module
Python interpreter (and programs which uses them, including NVDA) is not exactly multithreaded. Because of internal issues, Python uses so-called global interpreter lock to prevent multiple threads from messing with each other. One way to bring true parallelism in Python is use of multiprocessing module (multiple Python interpreters, each one devoted to a single task), which has its own advantages and drawbacks (NVDA does not ship with multiprocessing module in the first place).
To manage threads, Python programs (including NVDA) use Python's threading module. This library includes various ways of managing threads, including defining which function can execute in a separate thread, coordinating sharing of information between threads (locks, semaphores (resource access counter) and so on), and letting a thread run its task after waiting for a while (called timers). Even with multiple threads defined, NVDA is mostly single-threaded (serial execution).
To use threads, a programmer will define the thread type (regular thread, timer and so on), define some properties and tell the thread which routine to execute. Once the thread is defined, the start (thread.start) method is called to let the thread do its work.
Threads in Studio app module
For the most part, Studio app module uses only one thread (NVDA's main thread) to do its job. However, there are times when multiple threads are used - up to three can be active at a time: NVDA's main thread (announcing status changes, alarms, Cart Explorer and others), microphone alarm (a timer) and library scan (a background thread). Another situation threads are used is when background encoder monitoring is enabled (see the encoder routines article for details and use of threads there).
The main reason for using threads is to prevent background tasks from blocking user input (commands will not work when a long running task is run from the main NVDA thread). This is more noticeable when library scan is active as you'll find out soon. For now, let's take a look at microphone alarm.
Microphone alarm: A timer waiting to do its work
Simply put, microphone alarm is a timer (akin to a countdown timer). When the microphone becomes active, Studio app module will tell a timer thread to come alive. This timer's only job is to play the alarm sound and display a warning message, and it will wait a while (microphone alarm value in seconds; for example, five seconds).
The master switch which flips the microphone alarm timer is:
alarm = threading.Timer(micAlarm, messageSound, args=[micAlarmWav, micAlarmMessage])
Where "micAlarm" denotes how long this timer will wait and the second argument is the task to be performed (messageSound function). If microphone alarm is off (value is 0), this switch will be left alone forever (turned off until you enable the alarm by specifying a value above 0).
However, microphone alarm is more than a timer: a unique feature of timers is responding to events (a cancel event, that is). When the microphone becomes active, microphone alarm timer will become active. If you happen to turn off your microphone before microphone alarm kicks in, NVDA instructs microphone alarm to quit (timer is canceled). In other words, the "real" master switch is status change, and one of the activities performed by name change event handler (event_nameChange function described earlier) is to manage microphone alarm timer via doExtraAction method (in fact, microphone alarm and Cart Explorer are managed from this function).
Library scan: a unique combination of Studio API and a background thread
When NVDA is told to keep an eye on background library scanning, it calls up another thread to perform this duty. This thread will ask Studio for number of items scanned so far and take appropriate action after scanning is complete (in fact, multiple helper functions are used).
The library scan routine is performed as follows:
1. NVDA will make sure you are not in Insert Tracks dialog (if you are, background library scan routine will not be invoked, as event_nameChange will perform this duty instead).
2. If you do close Insert Tracks while a scan is in progress, or invoke library scan from SPL Assistant (Shift+R), NVDA will instruct a thread to keep an eye on scan progress in the background(see below for signature) to allow you to use Studio commands and to let you hear scan progress from other programs.
3. Library scan thread will ask Studio to return number of items scanned (this is done every second) and will store the result for record keeping.
4. After the scan result is obtained, the thread will check where you are in studio, and if you are back in Insert Tracks dialog, the thread will terminate (see step 1).
5. Every five seconds, library scan thread will call a private function (which wants to see how many items were scanned and current library scan announcement setting) to announce library scan results as follows:
A. If you tell NVDA to announce scan progress, NVDA will say, "scanning" and/or play a beep (if told to do so).
B. If NVDA is told to announce scan count, number of items scanned so far will be announced (again with or without a beep).
C. This reporter function will not be invoked if you tell NVDA to ignore library scan completely or ask it to interupt you only when the overall scan is complete (you can press Alt+NVDA+R to cycle through different library scan announcement settings).
6. Once library scanning is complete (after checking scan result value every second and seeing that the previous scan result and the current one have same values), NVDA will announce scan results (in some cases, number of items scanned will be announced).
You can imagine what would have happened if the above operation was not a background task: cannot perform other NvDA commands until library scan is complete, cannot cancel this operation and what not. And this is the signature of the thread that performs the above operation:
libraryScanner = threading.Thread(target=self.libraryScanReporter, args=(_SPLWin, countA, countB, parem))
There are important arguments in use: the function (task) to be performed and arguments for this function. The most important argument is the last one: Studio 5.0x and 5.10 expects different arguments when told to report number of items scanned so far.
Conclusion
Despite limitations of Python's threading routines, if used properly, it can open new possibilities, and you saw some of them above: microphone alarm and background library scan. Use of threads in the Studio app module also allows NvDA to be responsive while using Studio and allows background tasks to be faithful to the tasks at hand. We'll come back to threads when we talk about encoder connection routines. There is a more "magical" feature we'll visit, and this is our next stop on the SPL Studio Add-on Internals: Cart Explorer.
References:
1. Thread (Wikipedia): https://en.wikipedia.org/wiki/Thread_(computing)
2. Multi-core processor (wikipedia): https://en.wikipedia.org/wiki/Multi-core_processor
3. Multi-core introduction, Intel Developer Zone, March 5, 2012: https://software.intel.com/en-us/articles/multi-core-introduction
4. Intel Core I7-5960X specifications (Intel ARK): http://ark.intel.com/products/82930/Intel-Core-i7-5960X-Processor-Extreme-Edition-20M-Cache-up-to-3_50-GHz
5. Intel Xeon E7-8895V3 specifications (Intel ARK): http://ark.intel.com/products/84689/Intel-Xeon-Processor-E7-8895-v3-45M-Cache-2_60-GHz
6. Global Interpreter Lock (Python Wiki): https://wiki.python.org/moin/GlobalInterpreterLock
7. Threading (Python documentation, Python Software Foundation): https://docs.python.org/2/library/threading.html
8. Multiprocessing (Python documentation, Python Software Foundation): https://docs.python.org/2/library/multiprocessing.html#module-multiprocessing
Hi,
Of all the work done on Studio add-on, one of them stands out the most: background tasks. I spent many hours and months perfecting this concept, read documentation on this feature and learned a lot through this experience. Today, this work is used in various parts of the Studio app module and beyond, and we'll take a look at two most important results of this work: library scan and microphone alarm.
Brief feature overview
When you are producing a live show, you may forget that your microphone is active. The microphone alarm feature lets NVDA notify you if microphone has been active for a while. This happens even if you are using another program.
Library scan comes in handy when you want to see the progress of a background library scan. Typically, you would initiate library scans from Insert Tracks dialog (press Control+Shift+R). NvDA will then tell you how the scan is going, and if you close Insert Tracks dialog, NVDA will continue to monitor library scans in the background.
But there's more to it than a simple description when it comes to looking at internals of these features. As you'll see, these features use a concept that is gaining traction: running multiple tasks at once, or at least try to emulate it. We'll visit this concept first before returning to our regularly scheduled program of describing the internals of the two features above.
Recent trends in computing: more and more processors in a single computer
A decade ago, people thought a single core CPU was enough to run multiple programs. This involved the processor spending small fraction of a second devoted to each program. Nowadays, it has become common to see desktops, laptops, smartphones and other small devices using at least two cores (termed multi-core; two cores is dubbed "dual core"). As of August 2015, many computers use processors with four cores (dubbed "quad core"), while enthusiasts prefer more cores (the current record holder for desktop computers (as of August 2015) is 8 (octa core), held by Intel Core I7-5960X, a 3 GHz unlocked processor which costs about a thousand dollars; for servers where more cores are used, the current record holder is a package of eight processors (octa processor) called Intel Xeon E7-8895V3, with each processor boasting eighteen cores with base speed of 2.6 GHz).
Despite the fact that many computers come equipped with multi-core processors, not all programs take advantage of this. Python interpreter is one of those programs, and since NVDA is a Python-based screen reader and due to its operational architecture, many of its operations cannot take advantage of multiple processors. Fortunately, Python provides a way to simulate this - run certain tasks in the background, and this is utilized by NVDA and some of its add-ons as you'll see in this article on library scan and microphone alarm.
A gentle introduction to threads: multiple tasks at once
During normal business hours, a program will run from beginning to end with some interuptions (keyboard input, switching to a different part of the program and so on). However, there are times when the program will need to work with many things simultaneously, such as calculating distance between many points, adding multiple numbers at once, comparing many pairs of strings and so on. Fortunately, a mechanism called threads allow a program to do multiple things simultaneously.
A thread is a procedure independent of other tasks. If one thread is busy with something, other threads can work on other tasks. The best analogy is multiple bank tellers in a bank: customers can talk to different tellers, with one teller working on updating customer records for a customer while another customer discusses fraudulent credit card charges with a different teller.
A thread can be involved with parts of a task, devoted to a single task or multiple tasks. For example, an antivirus program could have multiple threads (workers) working independently of each other. One worker can display the overall progress of a scan, while other threads can scan multiple drives at once, with each thread devoted to scanning files and folders on separate drives. In NVDA world, multiple workers are involved to perform various tasks, including making sure NVDA is responsive, handling browse mode in different web browsers and so on.
Threads: a more geeky introduction
A thread (sometimes termed "thread of execution) is an independent path of execution. A single process (app) can have as many threads as it desires (minimum is one for the main thread). Each thread can be asked to perform certain operations with other threads in parallel, which can range from a single, repetative task (part of a function) to being responsible for an entire module or a significant part of the program. In case of antivirus example above, each scanner thread is responsible for scanning an entire drive, with each of them reporting its progress to a manager thread which displays overall progress of a virus scan.
Using threads means each thread can execute on a processor core on a multi-core system. Because of this, many people would want many programs to take advantage of this and finish their jobs faster. However, threads introduce disadvantages, namely many days spent designing careful coordination routines between threads, preventing attempts by multiple threads to change a critical value that a manager thread depends on (called race condition) and so forth.
Python's way of managing threads and the threading module
Python interpreter (and programs which uses them, including NVDA) is not exactly multithreaded. Because of internal issues, Python uses so-called global interpreter lock to prevent multiple threads from messing with each other. One way to bring true parallelism in Python is use of multiprocessing module (multiple Python interpreters, each one devoted to a single task), which has its own advantages and drawbacks (NVDA does not ship with multiprocessing module in the first place).
To manage threads, Python programs (including NVDA) use Python's threading module. This library includes various ways of managing threads, including defining which function can execute in a separate thread, coordinating sharing of information between threads (locks, semaphores (resource access counter) and so on), and letting a thread run its task after waiting for a while (called timers). Even with multiple threads defined, NVDA is mostly single-threaded (serial execution).
To use threads, a programmer will define the thread type (regular thread, timer and so on), define some properties and tell the thread which routine to execute. Once the thread is defined, the start (thread.start) method is called to let the thread do its work.
Threads in Studio app module
For the most part, Studio app module uses only one thread (NVDA's main thread) to do its job. However, there are times when multiple threads are used - up to three can be active at a time: NVDA's main thread (announcing status changes, alarms, Cart Explorer and others), microphone alarm (a timer) and library scan (a background thread). Another situation threads are used is when background encoder monitoring is enabled (see the encoder routines article for details and use of threads there).
The main reason for using threads is to prevent background tasks from blocking user input (commands will not work when a long running task is run from the main NVDA thread). This is more noticeable when library scan is active as you'll find out soon. For now, let's take a look at microphone alarm.
Microphone alarm: A timer waiting to do its work
Simply put, microphone alarm is a timer (akin to a countdown timer). When the microphone becomes active, Studio app module will tell a timer thread to come alive. This timer's only job is to play the alarm sound and display a warning message, and it will wait a while (microphone alarm value in seconds; for example, five seconds).
The master switch which flips the microphone alarm timer is:
alarm = threading.Timer(micAlarm, messageSound, args=[micAlarmWav, micAlarmMessage])
Where "micAlarm" denotes how long this timer will wait and the second argument is the task to be performed (messageSound function). If microphone alarm is off (value is 0), this switch will be left alone forever (turned off until you enable the alarm by specifying a value above 0).
However, microphone alarm is more than a timer: a unique feature of timers is responding to events (a cancel event, that is). When the microphone becomes active, microphone alarm timer will become active. If you happen to turn off your microphone before microphone alarm kicks in, NVDA instructs microphone alarm to quit (timer is canceled). In other words, the "real" master switch is status change, and one of the activities performed by name change event handler (event_nameChange function described earlier) is to manage microphone alarm timer via doExtraAction method (in fact, microphone alarm and Cart Explorer are managed from this function).
Library scan: a unique combination of Studio API and a background thread
When NVDA is told to keep an eye on background library scanning, it calls up another thread to perform this duty. This thread will ask Studio for number of items scanned so far and take appropriate action after scanning is complete (in fact, multiple helper functions are used).
The library scan routine is performed as follows:
1. NVDA will make sure you are not in Insert Tracks dialog (if you are, background library scan routine will not be invoked, as event_nameChange will perform this duty instead).
2. If you do close Insert Tracks while a scan is in progress, or invoke library scan from SPL Assistant (Shift+R), NVDA will instruct a thread to keep an eye on scan progress in the background(see below for signature) to allow you to use Studio commands and to let you hear scan progress from other programs.
3. Library scan thread will ask Studio to return number of items scanned (this is done every second) and will store the result for record keeping.
4. After the scan result is obtained, the thread will check where you are in studio, and if you are back in Insert Tracks dialog, the thread will terminate (see step 1).
5. Every five seconds, library scan thread will call a private function (which wants to see how many items were scanned and current library scan announcement setting) to announce library scan results as follows:
A. If you tell NVDA to announce scan progress, NVDA will say, "scanning" and/or play a beep (if told to do so).
B. If NVDA is told to announce scan count, number of items scanned so far will be announced (again with or without a beep).
C. This reporter function will not be invoked if you tell NVDA to ignore library scan completely or ask it to interupt you only when the overall scan is complete (you can press Alt+NVDA+R to cycle through different library scan announcement settings).
6. Once library scanning is complete (after checking scan result value every second and seeing that the previous scan result and the current one have same values), NVDA will announce scan results (in some cases, number of items scanned will be announced).
You can imagine what would have happened if the above operation was not a background task: cannot perform other NvDA commands until library scan is complete, cannot cancel this operation and what not. And this is the signature of the thread that performs the above operation:
libraryScanner = threading.Thread(target=self.libraryScanReporter, args=(_SPLWin, countA, countB, parem))
There are important arguments in use: the function (task) to be performed and arguments for this function. The most important argument is the last one: Studio 5.0x and 5.10 expects different arguments when told to report number of items scanned so far.
Conclusion
Despite limitations of Python's threading routines, if used properly, it can open new possibilities, and you saw some of them above: microphone alarm and background library scan. Use of threads in the Studio app module also allows NvDA to be responsive while using Studio and allows background tasks to be faithful to the tasks at hand. We'll come back to threads when we talk about encoder connection routines. There is a more "magical" feature we'll visit, and this is our next stop on the SPL Studio Add-on Internals: Cart Explorer.
References:
1. Thread (Wikipedia): https://en.wikipedia.org/wiki/Thread_(computing)
2. Multi-core processor (wikipedia): https://en.wikipedia.org/wiki/Multi-core_processor
3. Multi-core introduction, Intel Developer Zone, March 5, 2012: https://software.intel.com/en-us/articles/multi-core-introduction
4. Intel Core I7-5960X specifications (Intel ARK): http://ark.intel.com/products/82930/Intel-Core-i7-5960X-Processor-Extreme-Edition-20M-Cache-up-to-3_50-GHz
5. Intel Xeon E7-8895V3 specifications (Intel ARK): http://ark.intel.com/products/84689/Intel-Xeon-Processor-E7-8895-v3-45M-Cache-2_60-GHz
6. Global Interpreter Lock (Python Wiki): https://wiki.python.org/moin/GlobalInterpreterLock
7. Threading (Python documentation, Python Software Foundation): https://docs.python.org/2/library/threading.html
8. Multiprocessing (Python documentation, Python Software Foundation): https://docs.python.org/2/library/multiprocessing.html#module-multiprocessing
Thursday, August 20, 2015
StationPlaylist Add-on Internals: Column routines and method resolution order: one routine, four features
Hi,
In the last article, you saw how overlay classes work, as well as how track items in Studio are defined and used. We'll continue our tour of track items by looking at column navigation feature and how it is used in various places. But before we get into that, we need to talk about how NVDA knows how certain commands apply in specific situations via method resolution order.
Method resolution order: locating commands and defining command scope
If multiple classes (objects) are defined, especially if inheritance is involved, it becomes hard to determine which method is which and where various methods are defined. This becomes complicated when two or more classes inherit from a single parent, or multiple inheritance is in use (Python supports both scenarios).
One way Python solves this is through Method Resolution Order (MRO). Simply put, when a method is to be used, it first looks at whether this method is defined in the object it is dealing with, and if not, will consult the parent of this object.
For example, suppose we have a list box that defines a scroll method, and a custom list box widget inherits from this list box (in effect, custom list box is a list box). To make matters slightly complicated, let's say the scroll method is not defined (not really defined) in the custom list box. Then when the user scrolls through the custom list box, Python will see that the custom list box does not have the scroll method, so it'll look at the parent (original list box) and use its scroll method (in this case, yes, Python will use the parent's scroll method).
In terms of NVDA, method resolution order comes in handy when dealing with overlay classes. This has wide ranging consequences, including ability to limit where certain commands can be used to not defining a command (setting the script bound to gesture to None), effectively forcing NVDA to look up a given gesture from the base class (parent). If NVDA cannot locate the command in question, it'll pass this to Windows, which then sends the command to the active program.
Finishing the puzzle: MRO and Studio's track items
As described in the previous article, Studio app module defines two classes for track items: one for Studio 5.0x and another for 5.10. In reality, all that matters is the former, with the 5.10 class providing custom routines on top of the 5.0x track item class. In case of MRO, Studio 5.10 will be consulted if 5.10 is in use, then NVDA will consult Studio track item. For 5.0x, only original track item will be consulted.
The contents of the track item class (appModules.splstudio.SPLTrackItem) are as follows:
* initOverlayClass: This is run when the track item is first encountered. This method checks if Track Dial (see below) is active, and if so, assigns left and right arrow keys to Track Dial functions.
* reportFocus: This is called when reporting track items to you (broadcaster). It's main job is to see if custom column order is defined (see below) and builds needed pieces if column order is specified.
* Track Dial methods: Various methods and scripts used by Track Dial are defined, including announcing column information, handling leftmost column and so on.
In addition, Studio 5.10 track item (appModules.splstudio.SPL510TrackItem) includes a script to announce changed state when SPACE is pressed (this is done by speaking and/or brailling track name (obj.name)).
Birth of Track Dial: from hesitation to possibilities
As I was writing the add-on, one of the top suggestions I received was ability to use enhanced arrow keys feature to review columns. This feature allows broadcasters using screen reader scripts to use arrow keys to review column information such as artist, duration and so on. As of time of this writing, all three screen reader scripts support this feature.
At first, I told broadcasters that this wasn't possible. My impression back then (summer 2014) was that I had to manipulate track description text (obj.description) to enable this possibility. But seeing how other screen readers implement this convinced me that it might be possible to implement this for NVDA users, thus I started researching this in fall 2014.
I started by looking for patterns in description text that could be used to give users an impression that column navigation was active (when it was not). I studied how Python handles regular expressions and manipulated substrings (str.find and slicing) with no satisfactory results. Then in 2015, while working on improving support for Studio's own stream encoder, I noticed that the encoder entries were SysListView32 objects (NVDAObjects.IAccessible.SysListView32). Careful study of this object, especially a method to retrieve column content, gave me an idea as to how to bring Track Dial to life.
The magic behind Track Dial: SysListView32 controls
SysListView32 controls are lists with items organized into columns. For example, certain apps use these controls to arrange entries into columns, such as in certain table-based apps, and in case of studio, displaying various status about encoders.
When these controls are encountered, NVDA allows you to use table navigation commands (Control+Alt+arrows) to navigate between columns, provided that there are child objects (columns) exposed by the accessibility API implementation in use. Table navigation commands are supplied by another class (behaviors.RowWithFakeNavigation; the behaviors mix-in includes things NVDA should perform in various scenarios, including terminal input and output, editable text handling (via a dedicated module) and so on). This is all possible thanks to a method in SysListView32 class (NVDA Core) that allows one to retrieve column text, and this became the engine for Track Dial and other column navigation facilities in the Studio add-on (I say "add-on" because column navigation is used by both Studio and Track Tool).
The column text retrieval routine (which lives in SysListView32 and a copy lives in splstudio.splmisc module) is as follows:
1. Features requiring text from a specific column will call a private function in splstudio.splmisc module, which will take the current object and the column index as parameters.
2. The column retriever first looks up the handle for the process where the control lives, then creates a place holder for the buffer to hold the column content.
3. Next, it looks at the size of the underlying sysListView32 control (ctypes.sizeof) and asks Windows to allocate storage for an internal SysListView32 control via kernel32.dll's VirtualAllocEx. This is needed to store the resulting column text. Same is done for another place holder to store the actual column text by calling VirtualAllocEx.
4. The retriever then creates an internal SysListView32 control used as a place holder to store column text, then asks Windows to tell the process where the column text lives to reveal the column text for the specified column (first calls WriteProcessMemory, sens a message via user32.dll's SendMessage to retrieve text length, then uses ReadProcessMemory to retrieve the actual column text if there is something to read). Once the column text is revealed, the retriever stores the text inside the text buffer (ctypes.create_unicode_buffer, allocated to store the resulting text).
5. Lastly, the retriever frees resources (VirtualFreeEx) and returns the just retrieved column text, which can be used by routines requesting this.
Column retrieval and navigation routines: four scenarios, one servant
The above routine is used not only in Track Dial, but is also employed by three other routines: Column Search, column announcement order and Track Tool app module. Let's tour how these four friends use this routine in more detail.
Track Dial: Navigating columns in track items
The column retriever routine is just one of the activities performed during Track Dial, and to see the beauty of this feature, assign a command to toggle Track Dial (you need to focus on the track item before opening Input Gestures dialog, as Track Dial is used by track items alone). Once you assign a command to toggle Track Dial and toggle this on, Studio will set a flag indicating that Track Dial is on, which causes left and right arrow keys to be assigned to column navigation commands (this flag is stored as part of the add-on configuration database). If you tell NVDA to play beeps for status announcements (see previous article), NVDA will play a high beep. Once you are done with Track Dial, press the just assigned command to turn off Track Dial, at which point left and right arrow keys return to their original functions, a low beep will be heard (if told to do so) and Track Dial flag will be cleared.
When navigating columns, NVDA will check if you are at the edge of the track row, and if so, it will play a beep and repeat the last column text. If not, NVDA will look at the column you wish to navigate to (stored in the app module), then it'll call the column text retriever to retrieve the column text.
To handle differences between Studio 5.0x and 5.10, each track item class informs NVDA as to how leftmost column should be handled. For Studio 5.0x, leftmost column is artist field (obj.name will be checked), while track checked status is "shown" in Studio 5.10 (obj.name will be announced).
Custom column announcement order: What to announce and how
Track Dial routine also allowed another top requests to come to life: column announcement order. This allows you (broadcaster) to hear columns in specific order and to exclude certain columns from being announced. This is housed in reportFocus method in the main track item class.
In order to use this, you must tell NVDA to not use screen order (add-on settings dialog). Then open column announcement dialog and check the columns you wish to hear, then use the columns list to set column announcement order. The column announcement order is a list box with two buttons: move up and down.
Once column order and included columns are defined, NVDA will use this information to build track item description text. This is done by repeatedly calling the column retriever routine for columns you wish to hear, then using the column order you defined to build parts of the description text (a combination of a list and str.join is used).
For example, if NVDA is told to announce title and artist (in that specific order), NVDA will first locate title, then will add artist information. This is then presented as, "Title: some title, Artist: some artist".
Column Search: Finding text in specific columns
In the previous article, I mentioned that a single dialog performs double duty when talking about Track Finder. We'll now tour the other side of the coin: Column Search.
Column Search dialog adds a second control to Track Finder: a list of columns. Once text is entered to be searched in a column, NVDA will use trackFinder routine (discussed earlier) to locate text in specific columns (I mentioned that trackFinder routine takes column as the argument, and this is where this argument comes in handy). The routine is same as Track Finder except it uses the above column retriever routine to locate column text, and once search is done, it'll either move you to a track item or present an error dialog.
So what causes one dialog to present both Track Finder and Column Search dialog? It's all thanks to the arguments passed into the find dialog constructor. The signature is:
splmisc.SPLFindDialog(parent, obj, text, title, columnSearch=False)
The last argument (columnSearch) determines which version of the dialog to present. The object (obj) is needed to tell NVDA where to begin the search and to call the track finder routine defined in the object's app module.
Track Tool: one routine, multiple app modules
Column retriever routine is not only employed by Studio app module, but is also used in Track Tool app module (part of the studio add-on). Track Tool's use of column retriever include Track Dial for Track Tool (same routine as the Studio app module and Studio must be running to use it) and announcing column information (Control+NVDA+1 through 8).
Conclusion
Of all the features in the StationPlaylist Studio add-on, column navigation is one of my favorites (besides Cart Explorer and encoder support and others). I enjoyed working with this routine and learned a few things about Windows API, as well as open possibilities not previously explored before, such as Track Tool and Column Search. I hope that you'll find column navigation commands to be useful in your broadcasts.
I would like to take this time to answer a question posed by some users and developers: Can NVDA be ported to other operating systems? No. The above column retriever routine is a prime example why this cannot be done easily: different operating systems use different API's, and porting NonVisual Desktop Access to other operating systems will involve significant architectural changes to use the new API's. In case of ReactOS, this isn't possible, as there are no stable foundation from which NVDA screen reader can exercise its full rights: accessibility API's are needed, stable driver development framework is needed, ability to run a program as a service must be ready and so on. Add to the fact that we have several add-ons relying on Windows API (including StationPlaylist Studio add-on) and you'll see the huge work involved in an attempt to port NVDA to other operating systems.
I think we talked about column routines too much today. There is another important concept we need to talk about next time: threads and how it helps Studio perform some of its important tasks: library scan and microphone alarm.
References:
1. Method Resolution Order, The History of Python, june 23, 2010. http://python-history.blogspot.com/2010/06/method-resolution-order.html
2. List View, Microsoft Developer Network: https://msdn.microsoft.com/en-us/library/windows/desktop/bb774737(v=vs.85).aspx
3. List View Messages, Microsoft Developer Network: https://msdn.microsoft.com/en-us/library/windows/desktop/ff485961(v=vs.85).aspx
4. List View Item structure, Microsoft Developer Network: https://msdn.microsoft.com/en-us/library/windows/desktop/bb774760(v=vs.85).aspx
5. VirtualAllocEx (kernel32.dll) reference (Windows API): https://msdn.microsoft.com/en-us/library/windows/desktop/aa366890(v=vs.85).aspx
6. VirtualFreeEx (kernel32.dll) reference (Windows API): https://msdn.microsoft.com/en-us/library/windows/desktop/aa366894(v=vs.85).aspx
7. WriteProcessMemory (kernel32.dll) reference (Windows API): https://msdn.microsoft.com/en-us/library/windows/desktop/ms681674(v=vs.85).aspx
8. ReadProcessMemory (kernel32.dll) reference (Windows API): https://msdn.microsoft.com/en-us/library/windows/desktop/ms680553(v=vs.85).aspx
9. Ctypes (Python documentation, Python Software Foundation): https://docs.python.org/2/library/ctypes.html
In the last article, you saw how overlay classes work, as well as how track items in Studio are defined and used. We'll continue our tour of track items by looking at column navigation feature and how it is used in various places. But before we get into that, we need to talk about how NVDA knows how certain commands apply in specific situations via method resolution order.
Method resolution order: locating commands and defining command scope
If multiple classes (objects) are defined, especially if inheritance is involved, it becomes hard to determine which method is which and where various methods are defined. This becomes complicated when two or more classes inherit from a single parent, or multiple inheritance is in use (Python supports both scenarios).
One way Python solves this is through Method Resolution Order (MRO). Simply put, when a method is to be used, it first looks at whether this method is defined in the object it is dealing with, and if not, will consult the parent of this object.
For example, suppose we have a list box that defines a scroll method, and a custom list box widget inherits from this list box (in effect, custom list box is a list box). To make matters slightly complicated, let's say the scroll method is not defined (not really defined) in the custom list box. Then when the user scrolls through the custom list box, Python will see that the custom list box does not have the scroll method, so it'll look at the parent (original list box) and use its scroll method (in this case, yes, Python will use the parent's scroll method).
In terms of NVDA, method resolution order comes in handy when dealing with overlay classes. This has wide ranging consequences, including ability to limit where certain commands can be used to not defining a command (setting the script bound to gesture to None), effectively forcing NVDA to look up a given gesture from the base class (parent). If NVDA cannot locate the command in question, it'll pass this to Windows, which then sends the command to the active program.
Finishing the puzzle: MRO and Studio's track items
As described in the previous article, Studio app module defines two classes for track items: one for Studio 5.0x and another for 5.10. In reality, all that matters is the former, with the 5.10 class providing custom routines on top of the 5.0x track item class. In case of MRO, Studio 5.10 will be consulted if 5.10 is in use, then NVDA will consult Studio track item. For 5.0x, only original track item will be consulted.
The contents of the track item class (appModules.splstudio.SPLTrackItem) are as follows:
* initOverlayClass: This is run when the track item is first encountered. This method checks if Track Dial (see below) is active, and if so, assigns left and right arrow keys to Track Dial functions.
* reportFocus: This is called when reporting track items to you (broadcaster). It's main job is to see if custom column order is defined (see below) and builds needed pieces if column order is specified.
* Track Dial methods: Various methods and scripts used by Track Dial are defined, including announcing column information, handling leftmost column and so on.
In addition, Studio 5.10 track item (appModules.splstudio.SPL510TrackItem) includes a script to announce changed state when SPACE is pressed (this is done by speaking and/or brailling track name (obj.name)).
Birth of Track Dial: from hesitation to possibilities
As I was writing the add-on, one of the top suggestions I received was ability to use enhanced arrow keys feature to review columns. This feature allows broadcasters using screen reader scripts to use arrow keys to review column information such as artist, duration and so on. As of time of this writing, all three screen reader scripts support this feature.
At first, I told broadcasters that this wasn't possible. My impression back then (summer 2014) was that I had to manipulate track description text (obj.description) to enable this possibility. But seeing how other screen readers implement this convinced me that it might be possible to implement this for NVDA users, thus I started researching this in fall 2014.
I started by looking for patterns in description text that could be used to give users an impression that column navigation was active (when it was not). I studied how Python handles regular expressions and manipulated substrings (str.find and slicing) with no satisfactory results. Then in 2015, while working on improving support for Studio's own stream encoder, I noticed that the encoder entries were SysListView32 objects (NVDAObjects.IAccessible.SysListView32). Careful study of this object, especially a method to retrieve column content, gave me an idea as to how to bring Track Dial to life.
The magic behind Track Dial: SysListView32 controls
SysListView32 controls are lists with items organized into columns. For example, certain apps use these controls to arrange entries into columns, such as in certain table-based apps, and in case of studio, displaying various status about encoders.
When these controls are encountered, NVDA allows you to use table navigation commands (Control+Alt+arrows) to navigate between columns, provided that there are child objects (columns) exposed by the accessibility API implementation in use. Table navigation commands are supplied by another class (behaviors.RowWithFakeNavigation; the behaviors mix-in includes things NVDA should perform in various scenarios, including terminal input and output, editable text handling (via a dedicated module) and so on). This is all possible thanks to a method in SysListView32 class (NVDA Core) that allows one to retrieve column text, and this became the engine for Track Dial and other column navigation facilities in the Studio add-on (I say "add-on" because column navigation is used by both Studio and Track Tool).
The column text retrieval routine (which lives in SysListView32 and a copy lives in splstudio.splmisc module) is as follows:
1. Features requiring text from a specific column will call a private function in splstudio.splmisc module, which will take the current object and the column index as parameters.
2. The column retriever first looks up the handle for the process where the control lives, then creates a place holder for the buffer to hold the column content.
3. Next, it looks at the size of the underlying sysListView32 control (ctypes.sizeof) and asks Windows to allocate storage for an internal SysListView32 control via kernel32.dll's VirtualAllocEx. This is needed to store the resulting column text. Same is done for another place holder to store the actual column text by calling VirtualAllocEx.
4. The retriever then creates an internal SysListView32 control used as a place holder to store column text, then asks Windows to tell the process where the column text lives to reveal the column text for the specified column (first calls WriteProcessMemory, sens a message via user32.dll's SendMessage to retrieve text length, then uses ReadProcessMemory to retrieve the actual column text if there is something to read). Once the column text is revealed, the retriever stores the text inside the text buffer (ctypes.create_unicode_buffer, allocated to store the resulting text).
5. Lastly, the retriever frees resources (VirtualFreeEx) and returns the just retrieved column text, which can be used by routines requesting this.
Column retrieval and navigation routines: four scenarios, one servant
The above routine is used not only in Track Dial, but is also employed by three other routines: Column Search, column announcement order and Track Tool app module. Let's tour how these four friends use this routine in more detail.
Track Dial: Navigating columns in track items
The column retriever routine is just one of the activities performed during Track Dial, and to see the beauty of this feature, assign a command to toggle Track Dial (you need to focus on the track item before opening Input Gestures dialog, as Track Dial is used by track items alone). Once you assign a command to toggle Track Dial and toggle this on, Studio will set a flag indicating that Track Dial is on, which causes left and right arrow keys to be assigned to column navigation commands (this flag is stored as part of the add-on configuration database). If you tell NVDA to play beeps for status announcements (see previous article), NVDA will play a high beep. Once you are done with Track Dial, press the just assigned command to turn off Track Dial, at which point left and right arrow keys return to their original functions, a low beep will be heard (if told to do so) and Track Dial flag will be cleared.
When navigating columns, NVDA will check if you are at the edge of the track row, and if so, it will play a beep and repeat the last column text. If not, NVDA will look at the column you wish to navigate to (stored in the app module), then it'll call the column text retriever to retrieve the column text.
To handle differences between Studio 5.0x and 5.10, each track item class informs NVDA as to how leftmost column should be handled. For Studio 5.0x, leftmost column is artist field (obj.name will be checked), while track checked status is "shown" in Studio 5.10 (obj.name will be announced).
Custom column announcement order: What to announce and how
Track Dial routine also allowed another top requests to come to life: column announcement order. This allows you (broadcaster) to hear columns in specific order and to exclude certain columns from being announced. This is housed in reportFocus method in the main track item class.
In order to use this, you must tell NVDA to not use screen order (add-on settings dialog). Then open column announcement dialog and check the columns you wish to hear, then use the columns list to set column announcement order. The column announcement order is a list box with two buttons: move up and down.
Once column order and included columns are defined, NVDA will use this information to build track item description text. This is done by repeatedly calling the column retriever routine for columns you wish to hear, then using the column order you defined to build parts of the description text (a combination of a list and str.join is used).
For example, if NVDA is told to announce title and artist (in that specific order), NVDA will first locate title, then will add artist information. This is then presented as, "Title: some title, Artist: some artist".
Column Search: Finding text in specific columns
In the previous article, I mentioned that a single dialog performs double duty when talking about Track Finder. We'll now tour the other side of the coin: Column Search.
Column Search dialog adds a second control to Track Finder: a list of columns. Once text is entered to be searched in a column, NVDA will use trackFinder routine (discussed earlier) to locate text in specific columns (I mentioned that trackFinder routine takes column as the argument, and this is where this argument comes in handy). The routine is same as Track Finder except it uses the above column retriever routine to locate column text, and once search is done, it'll either move you to a track item or present an error dialog.
So what causes one dialog to present both Track Finder and Column Search dialog? It's all thanks to the arguments passed into the find dialog constructor. The signature is:
splmisc.SPLFindDialog(parent, obj, text, title, columnSearch=False)
The last argument (columnSearch) determines which version of the dialog to present. The object (obj) is needed to tell NVDA where to begin the search and to call the track finder routine defined in the object's app module.
Track Tool: one routine, multiple app modules
Column retriever routine is not only employed by Studio app module, but is also used in Track Tool app module (part of the studio add-on). Track Tool's use of column retriever include Track Dial for Track Tool (same routine as the Studio app module and Studio must be running to use it) and announcing column information (Control+NVDA+1 through 8).
Conclusion
Of all the features in the StationPlaylist Studio add-on, column navigation is one of my favorites (besides Cart Explorer and encoder support and others). I enjoyed working with this routine and learned a few things about Windows API, as well as open possibilities not previously explored before, such as Track Tool and Column Search. I hope that you'll find column navigation commands to be useful in your broadcasts.
I would like to take this time to answer a question posed by some users and developers: Can NVDA be ported to other operating systems? No. The above column retriever routine is a prime example why this cannot be done easily: different operating systems use different API's, and porting NonVisual Desktop Access to other operating systems will involve significant architectural changes to use the new API's. In case of ReactOS, this isn't possible, as there are no stable foundation from which NVDA screen reader can exercise its full rights: accessibility API's are needed, stable driver development framework is needed, ability to run a program as a service must be ready and so on. Add to the fact that we have several add-ons relying on Windows API (including StationPlaylist Studio add-on) and you'll see the huge work involved in an attempt to port NVDA to other operating systems.
I think we talked about column routines too much today. There is another important concept we need to talk about next time: threads and how it helps Studio perform some of its important tasks: library scan and microphone alarm.
References:
1. Method Resolution Order, The History of Python, june 23, 2010. http://python-history.blogspot.com/2010/06/method-resolution-order.html
2. List View, Microsoft Developer Network: https://msdn.microsoft.com/en-us/library/windows/desktop/bb774737(v=vs.85).aspx
3. List View Messages, Microsoft Developer Network: https://msdn.microsoft.com/en-us/library/windows/desktop/ff485961(v=vs.85).aspx
4. List View Item structure, Microsoft Developer Network: https://msdn.microsoft.com/en-us/library/windows/desktop/bb774760(v=vs.85).aspx
5. VirtualAllocEx (kernel32.dll) reference (Windows API): https://msdn.microsoft.com/en-us/library/windows/desktop/aa366890(v=vs.85).aspx
6. VirtualFreeEx (kernel32.dll) reference (Windows API): https://msdn.microsoft.com/en-us/library/windows/desktop/aa366894(v=vs.85).aspx
7. WriteProcessMemory (kernel32.dll) reference (Windows API): https://msdn.microsoft.com/en-us/library/windows/desktop/ms681674(v=vs.85).aspx
8. ReadProcessMemory (kernel32.dll) reference (Windows API): https://msdn.microsoft.com/en-us/library/windows/desktop/ms680553(v=vs.85).aspx
9. Ctypes (Python documentation, Python Software Foundation): https://docs.python.org/2/library/ctypes.html
StationPlaylist Add-on Internals: Track items, overlay classes and Track Finder
Hi,
When you use the Studio add-on for NVDA, you may have noticed that you can perform certain commands while focused on track items, and that the command to find tracks is same as that of find command in web browsers. If you are curious about these, then this installment will let you see how it works. But first, we need to go over some more facts about NVDA screen reader, this time we'll talk about objects.
Important facts about NVDA's use of objects
One of the questions I and other add-on authors and NVDA developers received was, "what exactly are objects and how are they used in NVDA?" In programming, an object is instance of the object definition coming to life (this definition, called a "class", defines how certain things behave and how information can be retrieved from this object by other parts of the program; the programming paradigm that uses classes and related concepts is termed "object-oriented programming"). For example, someone may say, "build me a phone book", and a programmer will think about how phone book entries are stored and come up with a "phone book" (an array of phone entries), all done via objects.
In graphical user interfaces (GUI's), an object is a more technical term for controls (sometimes called widgets). This includes windows, form fields, links, documents and so on. A control (object) can convey information such as state of the control, location, color and so on (the control contains both visible and internal attributes that can be used by other programs).
In NVDA world, all screen elements (controls) are objects. As such, when dealing with objects, NVDA uses accessibility API's to obtain needed information. To provide consistent user experience, differences between accessibility API's (IAccessible, UI Automation, Java Access Bridge and so on) are checked and provides a mechanism to announce same information across controls implemented using different frameworks. For example, when a check box is checked, NVDA will say "checked"" - NVDA will know if you checked this box because the underlying accessibility API informs NVDA of this change, and the same information is spoken regardless of whether it is dealing with IAccessible, UIA and so on.
Currently, NVDA can work with IAccessible, User Interface Automation (UIA), Java Access Bridge (JAB) and others (WAI ARIA is supported). Support modules for these API's lives in source/NVDAObjects directory of the NVDA Core source code.
Overlay classes: Customizing built-in objects
If NVDA was limited to using its own object handlers, we would be limited to information that is correctly exposed by accessibility API's (no app modules at all). But why is that NVDA can announce extra information for some controls and comes with various app modules for different applications? This is done through overlay classes - custom objects and their handlers built on top of API classes (built-in objects).
In essence, overlay classes are subclasses of stable API classes (subclasses are specialist classes deriving (inheriting) from one or more parent classes). This allows custom (overlay) objects to provide extra properties, ranging from control-specific commands to removing certain properties. For example, here's how NVDA's way of announcing toast notifications (Windows 8.x and 10) works:
1. Toasts are notifications from apps, and they are UIA objects (NVDAObjects.UIA.Toast).
2. When events fired by toasts are received by NVDA, it'll check to make sure it is dealing with toast notifications.
3. When NVDA is dealing with toasts, it'll perform what it is told to do by toast objects (announce toasts provided by "report help balloons" is checked from Object Presentation dialog).
Why do objects and overlay classes matter in Studio app module?
Some readers might ask this question after reading the above section on overlay classes. I had to introduce overlay classes because they are important in Studio app module: track items in playlist Viewer are overlay classes. In fact, there are two of them: track items for studio 5.0x (appModules.splstudio.SPLTrackItem) and 5.10 (appModules.splstudio.SPL510TrackItem; in case of 5.10, it derives its power from track item class for Studio 5.0x, which in turn is powered by IAccessible).
These classes were born when I started working on Studio 5.10 support in 2014. Because Studio 5.10 uses a different way of showing track properties, I had to come up with a way to take care of them. Adding to the urgency was the fact that Studio 5.10 uses check marks to indicate whether a track is selected for playback (Studio 5.0x and earlier uses check boxes), and when check marks are checked in Studio 5.10, NVDA would not announce newly checked state, fixed by defining a routine to be used when SPACE is pressed (via an overlay class). In addition, Track Finder (see below) was sensitive to object description changes, I modified it to account for differences between Studio versions.
Then in 2015, when I was designing Track Dial (next article), I thought about scope of this feature. I thought, "if I let this be invoked from everywhere, it could lead to issues such as errors and attempting to use Track Dial from somewhere other than track items". Then I thought, "perhaps I should limit this feature to main playlist viewer at the cost of making sure I identify track items correctly". Given that I had experience with overlay classes and since there was already an overlay class for Studio 5.10 track items, I decided to go with the latter option, which led to defining a new overlay class for Studio 5.0x track items and letting 5.10 track items inherit from this new class.
Track items overview
Each track item in Studio's playlist viewer consists of a row of columns (6 for Studio 5.0x and earlier, 18 for 5.10). As far as NVDA is concerned, it is an overlay class that provides a number of services, including:
* Track Dial toggle and announcing column information.
* Announcing columns in specific order (see the next article on importance of column navigation).
* For studio 5.10, a routine to handle when check marks are checked (when you check a track by pressing SPACE, NVDA will announce the newly checked state and will update the braille display accordingly).
We'll come back to track items when talking about columns in the next article. For now, let's move onto a related feature in Studio app module that works with track items: Track Finder.
Track Finder: Locating tracks given a search string
Track Finder allows you to search for tracks with the given artist or song title. This is done by performing a "linear search" - examining one track item to the next until the search term is found. This feature was partly inspired by similar features in other screen readers and NVDA's own find facility (cursorManager.FindDialog and its friends).
Track finder is not limited to searching for artist or title: a variation of this dialog (called Column Search) allows you to search for text in specific columns such as duration, file name and so on. In reality, it is a single dialog (splmisc.SPLFindDialog) that presents two dialogs (does this sound familiar?). For now, we'll talk about how the original Track Finder works (stay tuned for the next article to learn more about Column Search).
Original track Finder: commands, routines and controls
To use Track Finder, press Control+NVDA+F (wait, I saw this command before). For anyone who are accustomed to NVDA's browse mode, this command would be familiar: find text in webpages. This command performs activities similar to alarm dialogs (see previous articles): after conditions are checked (making sure you are in playlist viewer and you have added at least one track) and setting required flags, NVDA opens Track Finder dialog where you can enter a search term. Click OK, and NVDA will then go through tracks (via trackFinder method) looking for search term in track descriptions (in case you are searching for artist in Studio 5.0x, NVDA will also check the name of the check box, as this holds artist name). Once a track with the search term is found, NVDA will move focus to it (wx.widget.SetFocus), and if not, an error dialog will be shown.
Two other commands are used as part of Track Finder: Find next and previous, assigned to NVDA+F3 and NVDA+Shift+F3, respectively (they also come from browse mode). When these commands are invoked, it'll check if you have searched for a term before, and if not, it'll open Track Finder dialog. If you have searched for a term before, NVDA will perform linear search with search direction specified (trackFinder method in the app module takes various parameters, and one of them is search direction).
The signature of trackFinder method (linear search routine) in the Studio app module is:
trackFinder(self, text, obj, directionForward=True, column=None)
Text is the search term, obj is where the search should begin, direction specifies search direction and column is used if Column Search is used (searching for text in specific columns).
Conclusion
You learned a lot in this article: objects, overlay classes, track items and Track Finder. You also saw how an add-on writer will design a feature and how a design philosophy influences feature development. But this is just a beginning: I'll revisit my notes when explaining how Track Dial and other column navigation commands work in the next article.
References:
1. Sinclair, Rob. Microsoft Active Accessibility architecture, Microsoft Developer Network, August 2000. https://msdn.microsoft.com/en-us/library/ms971310.aspx
2. UI Automation Overview, Microsoft Developer Network. https://msdn.microsoft.com/en-us/library/ms747327(v=vs.110).aspx
3. Java Access Bridge overview, Java SE Desktop Accessibility, Oracle. http://www.oracle.com/technetwork/articles/javase/index-jsp-136191.html
4. Introduction to OOP (Object-Oriented Programming) with Python, Voidspace. http://www.voidspace.org.uk/python/articles/OOP.shtml
5. Non-Programmer's Tutorial for Python 3/Intro to Object Oriented Programming in Python 3 - Wikibooks. https://en.wikibooks.org/wiki/Non-Programmer%27s_Tutorial_for_Python_3/Intro_to_Object_Oriented_Programming_in_Python_3
When you use the Studio add-on for NVDA, you may have noticed that you can perform certain commands while focused on track items, and that the command to find tracks is same as that of find command in web browsers. If you are curious about these, then this installment will let you see how it works. But first, we need to go over some more facts about NVDA screen reader, this time we'll talk about objects.
Important facts about NVDA's use of objects
One of the questions I and other add-on authors and NVDA developers received was, "what exactly are objects and how are they used in NVDA?" In programming, an object is instance of the object definition coming to life (this definition, called a "class", defines how certain things behave and how information can be retrieved from this object by other parts of the program; the programming paradigm that uses classes and related concepts is termed "object-oriented programming"). For example, someone may say, "build me a phone book", and a programmer will think about how phone book entries are stored and come up with a "phone book" (an array of phone entries), all done via objects.
In graphical user interfaces (GUI's), an object is a more technical term for controls (sometimes called widgets). This includes windows, form fields, links, documents and so on. A control (object) can convey information such as state of the control, location, color and so on (the control contains both visible and internal attributes that can be used by other programs).
In NVDA world, all screen elements (controls) are objects. As such, when dealing with objects, NVDA uses accessibility API's to obtain needed information. To provide consistent user experience, differences between accessibility API's (IAccessible, UI Automation, Java Access Bridge and so on) are checked and provides a mechanism to announce same information across controls implemented using different frameworks. For example, when a check box is checked, NVDA will say "checked"" - NVDA will know if you checked this box because the underlying accessibility API informs NVDA of this change, and the same information is spoken regardless of whether it is dealing with IAccessible, UIA and so on.
Currently, NVDA can work with IAccessible, User Interface Automation (UIA), Java Access Bridge (JAB) and others (WAI ARIA is supported). Support modules for these API's lives in source/NVDAObjects directory of the NVDA Core source code.
Overlay classes: Customizing built-in objects
If NVDA was limited to using its own object handlers, we would be limited to information that is correctly exposed by accessibility API's (no app modules at all). But why is that NVDA can announce extra information for some controls and comes with various app modules for different applications? This is done through overlay classes - custom objects and their handlers built on top of API classes (built-in objects).
In essence, overlay classes are subclasses of stable API classes (subclasses are specialist classes deriving (inheriting) from one or more parent classes). This allows custom (overlay) objects to provide extra properties, ranging from control-specific commands to removing certain properties. For example, here's how NVDA's way of announcing toast notifications (Windows 8.x and 10) works:
1. Toasts are notifications from apps, and they are UIA objects (NVDAObjects.UIA.Toast).
2. When events fired by toasts are received by NVDA, it'll check to make sure it is dealing with toast notifications.
3. When NVDA is dealing with toasts, it'll perform what it is told to do by toast objects (announce toasts provided by "report help balloons" is checked from Object Presentation dialog).
Why do objects and overlay classes matter in Studio app module?
Some readers might ask this question after reading the above section on overlay classes. I had to introduce overlay classes because they are important in Studio app module: track items in playlist Viewer are overlay classes. In fact, there are two of them: track items for studio 5.0x (appModules.splstudio.SPLTrackItem) and 5.10 (appModules.splstudio.SPL510TrackItem; in case of 5.10, it derives its power from track item class for Studio 5.0x, which in turn is powered by IAccessible).
These classes were born when I started working on Studio 5.10 support in 2014. Because Studio 5.10 uses a different way of showing track properties, I had to come up with a way to take care of them. Adding to the urgency was the fact that Studio 5.10 uses check marks to indicate whether a track is selected for playback (Studio 5.0x and earlier uses check boxes), and when check marks are checked in Studio 5.10, NVDA would not announce newly checked state, fixed by defining a routine to be used when SPACE is pressed (via an overlay class). In addition, Track Finder (see below) was sensitive to object description changes, I modified it to account for differences between Studio versions.
Then in 2015, when I was designing Track Dial (next article), I thought about scope of this feature. I thought, "if I let this be invoked from everywhere, it could lead to issues such as errors and attempting to use Track Dial from somewhere other than track items". Then I thought, "perhaps I should limit this feature to main playlist viewer at the cost of making sure I identify track items correctly". Given that I had experience with overlay classes and since there was already an overlay class for Studio 5.10 track items, I decided to go with the latter option, which led to defining a new overlay class for Studio 5.0x track items and letting 5.10 track items inherit from this new class.
Track items overview
Each track item in Studio's playlist viewer consists of a row of columns (6 for Studio 5.0x and earlier, 18 for 5.10). As far as NVDA is concerned, it is an overlay class that provides a number of services, including:
* Track Dial toggle and announcing column information.
* Announcing columns in specific order (see the next article on importance of column navigation).
* For studio 5.10, a routine to handle when check marks are checked (when you check a track by pressing SPACE, NVDA will announce the newly checked state and will update the braille display accordingly).
We'll come back to track items when talking about columns in the next article. For now, let's move onto a related feature in Studio app module that works with track items: Track Finder.
Track Finder: Locating tracks given a search string
Track Finder allows you to search for tracks with the given artist or song title. This is done by performing a "linear search" - examining one track item to the next until the search term is found. This feature was partly inspired by similar features in other screen readers and NVDA's own find facility (cursorManager.FindDialog and its friends).
Track finder is not limited to searching for artist or title: a variation of this dialog (called Column Search) allows you to search for text in specific columns such as duration, file name and so on. In reality, it is a single dialog (splmisc.SPLFindDialog) that presents two dialogs (does this sound familiar?). For now, we'll talk about how the original Track Finder works (stay tuned for the next article to learn more about Column Search).
Original track Finder: commands, routines and controls
To use Track Finder, press Control+NVDA+F (wait, I saw this command before). For anyone who are accustomed to NVDA's browse mode, this command would be familiar: find text in webpages. This command performs activities similar to alarm dialogs (see previous articles): after conditions are checked (making sure you are in playlist viewer and you have added at least one track) and setting required flags, NVDA opens Track Finder dialog where you can enter a search term. Click OK, and NVDA will then go through tracks (via trackFinder method) looking for search term in track descriptions (in case you are searching for artist in Studio 5.0x, NVDA will also check the name of the check box, as this holds artist name). Once a track with the search term is found, NVDA will move focus to it (wx.widget.SetFocus), and if not, an error dialog will be shown.
Two other commands are used as part of Track Finder: Find next and previous, assigned to NVDA+F3 and NVDA+Shift+F3, respectively (they also come from browse mode). When these commands are invoked, it'll check if you have searched for a term before, and if not, it'll open Track Finder dialog. If you have searched for a term before, NVDA will perform linear search with search direction specified (trackFinder method in the app module takes various parameters, and one of them is search direction).
The signature of trackFinder method (linear search routine) in the Studio app module is:
trackFinder(self, text, obj, directionForward=True, column=None)
Text is the search term, obj is where the search should begin, direction specifies search direction and column is used if Column Search is used (searching for text in specific columns).
Conclusion
You learned a lot in this article: objects, overlay classes, track items and Track Finder. You also saw how an add-on writer will design a feature and how a design philosophy influences feature development. But this is just a beginning: I'll revisit my notes when explaining how Track Dial and other column navigation commands work in the next article.
References:
1. Sinclair, Rob. Microsoft Active Accessibility architecture, Microsoft Developer Network, August 2000. https://msdn.microsoft.com/en-us/library/ms971310.aspx
2. UI Automation Overview, Microsoft Developer Network. https://msdn.microsoft.com/en-us/library/ms747327(v=vs.110).aspx
3. Java Access Bridge overview, Java SE Desktop Accessibility, Oracle. http://www.oracle.com/technetwork/articles/javase/index-jsp-136191.html
4. Introduction to OOP (Object-Oriented Programming) with Python, Voidspace. http://www.voidspace.org.uk/python/articles/OOP.shtml
5. Non-Programmer's Tutorial for Python 3/Intro to Object Oriented Programming in Python 3 - Wikibooks. https://en.wikibooks.org/wiki/Non-Programmer%27s_Tutorial_for_Python_3/Intro_to_Object_Oriented_Programming_in_Python_3
Subscribe to:
Posts (Atom)