Did you ever need to impersonate the role of the sound-effects man in the shadow-theatre show at your young child's nursery school? It happened to me... and - trust me - I had some trouble while figuring out how many different sounds and noises I was expected to produce in the exact moment they were needed to accompany the show actions. So, I started to think about a way to create a mixing sounds keyboard, capable of playing different sound files (.WAV, .MP3, and so on) associated to each key, making me able to reproduce them by simply pressing a key when the corresponding noise was needed by an actor/animator.
Even if I'm sure lots of programs exist to accomplish this goal, I decided to create my own in VB.NET. The simple utility presented here, named "Sampled Sounds Player", creates an association matrix between files containing sampled sounds and keyboard keys, and lets you play those files (also looping and overlapping them) by programmatically controlling the Windows Media Player.
How the utility works
The utility is very simple: it's a single-form VB.NET Windows Application basically, containing a
DataGrid that shows the filename/key association you chose. This association is saved in a configuration XML file resulting from the
WriteXML() method invocation on the
DataSet underlying the grid. This XML file is editable also from the user interface of the grid itself. We'll call this kind of files "sounds maps", because they actually simply contain the mapping between sound files and keyboard keys.
As stated previously, this utility plays sound files by using Windows Media Player (version 10, for example). To programmatically control Windows Media Player, I added a project reference to the
WMPLib.DLL component (that is a non-managed code library, so Visual Studio created for me the needed interoperability assembly). The
WMPLib library is easy to use; it exposes the
WindowsMediaPlayerClass class that represents an instance of a player, on which you can invoke the same actions of the interactive player: you can programmatically load a file to be played, start/stop/pause the playing, set the "loop" property to repeat the sound, and so on.
My goal was to embed the logic of controlling sounds inside the grid shown on the user interface. Then, I created the
RunningSound class, that represents a sound ready to be played: this class wraps the
WindowsMediaPlayerClass, and is designed to be used directly as a datatype of the
DataTable underlying the UI grid. So, for example, it exposes a custom
ToString() method, used to show the current status of the sound being played as a field in the grid table. In fact, while using the "Sampled Sounds Player", it's important to keep track of the sounds currently playing, as in the following picture, where you see the grid showing this situation:
- the sounds associated to keys "P" and "U" are being played, the sound "U" played continuously (note the infinite loop symbol near the play symbol);
- the sounds associated to keys "J" and "L" are currently paused (being the "L" sound set for repeated play);
- all other sounds are ready to be played, just by pressing the corresponding key.
Sounds statuses are visually updated on the grid by intercepting the
PlayStateChange event of each instantiated player. This stuff is, again, carried out by the
RunningSound wrapper class, capable of accessing the
DataRow currently hosting the actual instance of the player that changed its state.
To play a sound when the user hits the corresponding key, I simply handled the form's
KeyDown event, after setting the form's
KeyPreview property to
True (notice that if the
DataGrid has the focus, the form is unable to fire that event handler). In the
KeyDown event handler, a simple lookup is performed on the table containing the sounds/keys mapping, and - if a row is found - the corresponing player is controlled. I decided to use the mapped keys in combination with the Shift and Control keys to perform different actions:
||PLAY / PAUSE the sound|
|CTRL + KEY
||STOP the sound|
|SHIFT + KEY
||Toggle LOOP ON/OFF|
The "Load", "Edit", and "Save" controls on the form allow you to:
- load a new sounds map on the grid;
- start the edit mode, that permits you to modify the grid content;
- save the currently shown grid as a sounds map file.
- when a new sounds map is loaded, the old one is discarded, and on each grid row, a new player with the corresponding sound is created;
- when edit mode starts, all sounds being played are automatically stopped and the corresponding players are unloaded;
- when edit mode ends, the grid reverts to the read-only/playing mode;
- when edit mode ends, all sounds and players are reloaded, complying with the modified grid;
- while editing, you can - as in a standard
DataGrid - add/delete/modify any grid row and any field but the
Status column, that is obviously always read-only;
- the "Save" button is enabled only in edit mode;
- when you save the grid, it automatically reverts to the read-only/playing mode;
- you can discard the changes you made on the grid by simply unchecking the "Edit" checkbox and loading again the sound map file previously saved;
- you can fill the
Filename field with the name of any file that Windows Media Player can play (.WAV, .MP3 and so on), but keep in mind that the file you specify must be located in the same folder where you saved the sound map file referencing it.
Points of interest
The main points in this utility are: the use of WMP and the manipulation of
DataSets. Being honest, controlling Windows Media Player is very easy: most of the magic is done by the component itself. The wrapper class I created and used directly as a
DataColumn datatype made the rest. Notice the way I managed the serialization issues about this "special"
DataSet: due to the nature of the
Status column (that hosts a custom
RunningSound datatype, that is redundant/dependant from the other data on the same row, and that is read-only), it is not serialized at all (in fact: it is suppressed when the grid is saved, and it is re-created when re-loading the sounds map later).
Currently, there is no check about duplicated entries in the
Key column (a simple
UNIQUE constraint could be enough to accomplish this).
Currently, all the sounds are played at the same volume level. Potentially, each instance of the Windows Media Player can be controlled on its playing volume, so it's not difficult to implement this enhancement. For this utility, the only challenge is about making each sound's volume controllable by a very simple keyboard input (an idea could be the use of up/down keys to modify the volume of the sound selected on the grid, also bypassing in some way the
KeyPreview issue on the
It could be very useful to add a feature capable to fill a new sounds map automatically from the contents of a given sound files folder (the
System.IO.Directory.GetFiles() method makes it trivial to implement this enhancement).
Many thanks to my friend Davide Melis for some initial "knowledge feeds" on the Windows Media Player technology.