Design and Implementation
iTuner is a WPF application with a minimal user interface. When started, iTuner presents itself as a notify icon in the Windows system tray. As iTunes playback begins, a notification window is displayed showing the current track information. Clicking the notify
icon opens the dashboard panel. Right-clicking the notify icon opens a context menu of common actions.
- WPF Stuff
- Visual Studio Project Configuration
- Images and Artwork
Apple was thoughtful enough to release a COM interface for iTunes; this is installed along with iTunes. Also available on their
is an SDK, which really just means a moderately adequate .chm help file. So once iTunes is installed, you simply need to add a reference to the iTunesLib Type Library in your project.
The first task peformed by iTuner is to determine if iTunes is active and if not then activate it. To determine if iTunes is active, we simply enumerate the processes on the system looking for the process name "iTunes". If found then we can continue.
However, if iTunes is not yet active, it may take a few seconds to fully initialize it so we want to display a splash screen to entertain the user while they're waiting. The splash screen is described a bit
The Controller class is a thin wrapper that encapsulates the an iTunesAppClass instance and provides easy binding points for our UI. Nothing fancy here, only simple C# properties and a basic implementation of INotifyPropertyChanged.
There are a couple of additional features of Controller. The lyrics discovery engine is maintained by Controller. There is also a one-second repeating timer that simply signals an OnPropertyChanged event for the current track. This drives updates for the UI,
partcularlly the TrackPanel control.
class is a simple sequential workflow engine. It maintains a FIFO queue that executes tasks, one at a time, while other tasks queue up and wait. This structure satisfies user requests while avoiding concurrency issues and simplifying otherwise
complex scenarios. For example, the Export scanner generates transient tracks while the Orphan scanner looks for files not yet registered in the library. It's possible for these two scanners to collide when the Export scanner removes the library entry
but has not yet moved its file to the export directory. This provides a window of opportunity for the Orphan scanner to discover the file and attempt to bring it back into the library, ultimately resulting in a dead track, which would ultimately be cleaned
up by the Dead Track scanner. Oy! So by simplifying the workflow, we simplify the overall house-keeping.
The sequential workflow is implemented by the BlockingQueue class. This thread-safe Queue<> extension blocks Dequeue requests until an Enqueue occurs. Blocking is synchronized using the static Wait and Pulse methods of System.Threading.Monitor.
The UI thread enqueues new scanner requests, while the Librarian patiently waits on a BackgroundWorker thread, in the DoWork method, blocked by BlockingQueue.Dequeue. As a new item traverses this pipeline, it is executed, recorded, and the Librarian waits for
the next request.
The scanners realize the IScanner interface and implement the ScannerBase class which declares an abstract Execute method that inheritors must implement.
class provides the mechanism for discovering song lyrics by querying online providers. As each musical track begins, a background worker is started. This worker queries each provider, one by one, until lyrics are retrieved for the track.
At the moment, a new background worker (a new thread) is created for each request, allowing concurrent request for multiple songs. Now that we have the Librarian, it may be possible to feed these requests into the same pipeline employed by the Librarian.
These would mean elevating that pipeline in the architecture so it would be above both the Librarian and the Lyric engine - probably a good idea! Something to think on...
The lyric providers are implementations of the abstract LyricsProviderBase
class which realized the
interface. The RetrieveLyrics method of this interface is the only entry point and is expected to complete synchronously, returning the lyrics as a String value. Each provider encapsulates all logic necessary for communicating with the
respective provider and parsing its response, normalizing return values into consistent format.
As LyricEngine class executes each provider, if an error is encountered, a counter is maintained. When a particular provider raises three consecutive failures, it is removed from contention and not called again until iTuner is restarted.
The global hotkey infrastructure has been part of Windows for quite some time. iTuner literally hooks into this infrastructure to register and intercept global hotkey notifications.
The iTuner implementation begins with the KeyManager
class. Besides the mundane
, and Save
methods, the interesting part here is the use of a System.Windows.Forms.NativeWindow realized as the
class. DriverWindow overrides the WndProc method to intercept global hotkeys, identified with the WM_HOTKEY constant. Once the WM_HOTKEY Msg is recognized, the LParam is pulled apart to extract the key code and modifier flags. Finally, a
KeyPressed event is raised to notify consumers - in this case the iTuner AppWindow class.
KeyManager is a very specific implementation intended to intercept hotkeys and notify consumers. This is not exactly fit-for-purpose for the the HotKeyEditor dialog. We also did not want to clutter this class with the mechanics necessary for interactive hotkey
modication. So the KeyTrapper
class is specifically suited to the needs of the HotKeyEditor. KeyTrapper differs from KeyManager in that it filters special system keystrokes that would otherwise make Windows entirely inoperable, such as Esc, Up, Down,
Left, Right, etc.
First of all let's take a look at the design. I'm not an expert designer. But I'm pretty good at taking an existing design and running with it. I wanted to make iTuner look like an extension of iTunes and that's why I've "borrowed"
its look and feel. (I'm hoping Apple's legal staff doesn't notice! But then again, I'm not gaining anything from this free open-source project, right? Right?)
WPF is a perfect foundation (is that redundant?) for creating the gradients, geometries, and visual effects I was after. The interesting aspects for WPF newbies may be the FadingWindow class and lots of use of the XAML Path element. The main window control
panel uses Path extensively.
The primary iTuner windows - AppWindow, TrayWindow, and AboutBox - all derive from FadingWindow. This base class provides fade-in and fade-out features. Fading is accomplished with DoubleAnimation storyboards. There were a couple of important lessons learned.
Inheritors must set the AnimatedElement property. This is the visual element that is to be animated using the storyboards. This cannot be the window itself and should instead be something like a main outer border.
While a FadingWindow is not shown, the Visiblity property is set to Visibility.Hidden. This excludes the window from the Windows Alt-Tab program switcher control. Otherwise, two entries appear in this control with transparent thumbnails.
Microsoft apparently either did not believe that a WPF based notify icon was important or does not see notify icons as part of its long-term strategy. There have been a few
using WPF and many more not-so-inspiring attempts wrapping the Windows.Forms NotifyIcon class. In the end, I whimped out and thought the WPF implementations too much for this
simple application and only needed a raw NotifyIcon anyway. What I really needed was access to the Windows taskbar to know which edge of the screen on which it was docked. This allows me to position the main window relative to the taskbar.
The iTuner.Taskbar class is a very thin wrapper of some Interop functions that provides two features. First, it determines the docking edge of the taskbar through the Taskbar.Dock instance property. Second, it calculates the closes point on the "inside"
edge of the taskbar, perpendicular to a specific icon in the system track; this is available from the GetSuggestedPosition instance method.
Once we have a docking edge and a relative starting point, we can then position the main window or about box window relative to that point.
Visual Studio Project Configuration
iTuner contains some low level Win32 Interop code such as hot key registration, that does not work when hosted within the Visual Studio hosting process used for debugging. For this reason, you must disable this by modifying the project properties. In the iTuner.csproj
settings, I've unchecked the Enable the Visual Studio hosting process
Why Two .config Files?
I wanted one configuration for developing and testing that would enable debugging and diagnostic logging but need a separate configuration to package into the production installer that disabled those modes. And I knew I'd forget to manually update the App.config
if I had to rely on only that one file. So I'm playing a trick with the iTunerSetup project as follows:
The iTuner project has two .config files, App.config and iTuner.exe.config. The App.config is used normally while the iTuner.exe.config file is set to "None" build action and "Do not copy" to output. Although the the setup project obviously
depends on the output of the iTuner project, including the App.config, it is configured to filter out the App.config file but include the iTuner.exe.config.
Using this trick, the iTuner project relies on App.config while the iTunerSetup project relies on iTuner.exe.config.
Images and Artwork
All of the image files used in the iTuner application were captured and at least moderately modified using
. The .pdn files in the Images folder are native Paint.NET files.
Copyright (c) 2010 Steven M. Cohn. All rights reserved.