Animated Musical Holiday Light Show - Version 2.0
- Posted: Dec 23, 2007 at 11:25 AM
- 5,862 Views
- 26 Comments
Loading User Information from Channel 9
Something went wrong getting user information from Channel 9
Loading User Information from MSDN
Something went wrong getting user information from MSDN
Loading Visual Studio Achievements
Something went wrong getting the Visual Studio Achievements
|Create an animated holiday light show for indoor or outdoor use using some Phidget Interface Kits, extension cords, and .NET.|
Time Required: 3-6 hours
Hardware: Phidget Interface Kit 0/0/4, extension cords, wire nuts, spare wire
Welcome to version 2.0 of the software and article! I have rewritten the key points of this article to address the new features in the latest version of the software. Read on for a full explanation...
New Features for 2.0
Note that any sequences created with the old version will continue to play just fine, but editing them could be troublesome due to the new timing method.
And now back to the article with updates. The hardware section remains the same, but the software discussion below is updated with new information.
I'm sure by now everyone with a computer has seen the videos of holiday light shows timed to holiday music such as this one. For this holiday season, I decided to create my own indoor show using some off-the-shelf components and .NET.
WARNING: The hardware portion of this project uses standard 120V AC current. As you are likely aware, this is enough voltage to seriously hurt or kill you. Please be careful and follow the instructions closely.
The hardware we are going to build will allow for one Phidget board to be plugged into a single AC outlet and provide 4 output outlets that can be switched off and on by the Phidget board's relays. By building two of these and placing them in a project box, I have a neat and tidy control box with 2 USB inputs, 2 AC male plugs for the wall, and 8 AC female plugs for my lights. The following description will be for building a single unit.
Let's start by preparing the extension cords. Cut the female end off of one extension cord. Split the cord up the center and strip the insulation off each of the wires to expose the ends. Twist the ends with your fingers to create a neat, twisted wire as shown:
Next, take the remaining four extension cords and cut the male ends off each. As before, split the cord up the center a small bit, strip off some insulation, and twist the exposed ends as shown:
Now, cut 4 equal lengths of your 14-16 gauge wire. These should be no longer than 2-4 inches in length. Strip some insulation off each end and twist up any loose wires.
For the next part, you will need to pay close attention. Each extension cord should have two different types of insulation around the wires. One side should have a ribbed edge, and one side should have a smooth edge shown below:
The ribbed side should be in line with the "fat" prong/receptacle (the neutral side) and the smooth side should be in line with the smaller prong/receptacle (the active/"hot" side). It is important that the next steps be followed carefully, noting which wires I am referring to. Using the wrong wire can lead to a short, blown fuses, kicked circuit breakers, or even worse things.
Take the 4 short wires you cut earlier and twist them all together along with the ribbed/neutral wire from the extension cord with the male end still attached.
Twist a wire nut over the exposed ends to keep them together and covered.
Next, twist together the smooth/"hot" wire from the four extension cords with the female ends still attached along with the smooth/"hot" wire from the extension cord with the male end attached (i.e. the other wire from the male cord used above).
Again, twist on a wire nut to keep things covered and safe.
You should now be left with the opposite ends of the 4 cut wires exposed, and the 4 ribbed/neutral wires of the female extension exposed. These will all be put into the screw terminals of the Phidget Interface Kit.
The Phidget Interface Kit board has 4 groups of screw terminals. Each group contains 3 items: NO, XC, and NC, where X is the relay number in question. These stand for "Normally Open", "Common", and "Normally Closed". For this project, the lights should normally be off and switched on via the software, so the NO (Normally Open) and XC (Common) ports will be used.
Place one wire from each exposed bundle into each NO and XC port. Note that it does not matter which wire you plug into which terminal of each group, just that each group has only one short wire and only one extension cord wire.
In the end, there will be one each of the 4 short wires in each group on the board, and one each of the extension cord wires in each group on the board as pictured:
Below is a very simple schematic of the wiring of a single board:
I decided to keep things neat and tidy and mount the Phidget boards into a project box. Since I have 2 boards to manage, I placed both inside a single, large box. I drilled holes in the short sides to expose the boards' USB ports. I then notched out some spaces on the top edge and lid for the extension cords to pass through. I mounted the boards inside the box with some carefully placed two-sided foam tape.
The finished product can be seen below:
And that's it! If this box will be placed outside, take the time to properly weatherproof the box so the elements cannot damage anything inside.
Your individual light strings will be plugged into each extension cord outlet. An average strand of mini-lights draws about .3 amps. An average string of larger bulbs will draw 1-2 amps. The relays on the Phidget board are rated at 10 amps. Additionally, a standard house circuit will allow up to 15-20 amps before overloading. Check the circuit breaker in your home on which the outlet you'll be using lives for the allowed amperage. Also, keep in mind that any other devices that are plugged into that circuit elsewhere in the house will be drawing power, so you may not be able to draw a full 15 amps from it. So, be sure to not draw more than 10A per channel, nor more than 15-20A in total, including all additional devices plugged into that circuit. Keep this in mind as you string your lights together on each channel.
The Light Sequencer application uses a grid-style interface to show the list of channels and when each channel is switched on and off.
Squares can be toggled on or off by highlighting them, right-clicking with the mouse and selecting On or Off from the context menu. They can also be toggled by pressing the O (for on) or F (for off) keys on the keyboard.
Additionally, sequences can be "recorded" by pressing the Record button in the toolbar. This will start the music and allow the user to tap out the rhythm for each channel by pressing the number key on the keyboard corresponding to the channel.
Sequences can be saved at any time and reloaded for editing or play with the Phidgets devices connected.
When I first started the software for this project, the first problem I ran into was the fact that the DataGridView control does no support multiple headers. As is shown in the screenshot above, the grid is broken down into seconds and then milliseconds. For display purposes. it is easier to break the header down into two segments, one showing the labeled second markers, and one showing the subdivisions per second.
To accomplish this, I created two DataGridViews: one for the header, which contains no data and is only as tall as the header row, and one for the sub-header and the data below it. This works great except for scrolling. To accomplish this, I simply listen for the Scroll event on the main grid and apply the scrolling offset to the header grid:
private void dgvMain_Scroll(object sender, ScrollEventArgs e)
dgvHeader.HorizontalScrollingOffset = e.NewValue;
Private Sub dgvMain_Scroll(ByVal sender As Object, ByVal e As ScrollEventArgs) Handles dgvMain.Scroll
dgvHeader.HorizontalScrollingOffset = e.NewValue
Additionally, I ran into some performance issues drawing the grid. At first, drawing a grid with so many columns was quite slow. By setting the grid's Visible property to false before adding the rows and columns and then returning the Visible property to true, the grid now draws quite quickly.
The next issue tackled was starting and stopping a music file. In version 2, the software supports sampled music (MP3, WAV, etc.) as well as MIDI files. As I was attempting to write a MIDI file parser and player (so I could have access to the internal data for auto-generating a sequence) I found a fantastic MIDI library written by Leslie Sanford. This library is used by the Light Sequencer application.
With two playback libraries in place, I created an interface which contains Start, Stop, Load, etc. methods so that the front-end could use any playback engine interchangeably. The MCIPlayback engine uses standard MCI commands for playing sampled music. Commands are executed by passing them to the mciSendString function exported by winmm.dll. In order to use this function from .NET, we must import the method and setup its signature as follows:
static extern Int32 mciSendString(String command, StringBuilder buffer, Int32 bufferSize, IntPtr hwndCallback);
Declare Function mciSendString Lib "winmm.dll" Alias "mciSendStringA" (ByVal command As String, _
ByVal buffer As StringBuilder, ByVal bufferSize As Int32, _
ByVal hwndCallback As IntPtr) As Int32
To open a music file, the open command is used as follows:
open "<path to file>" type mpegvideo alias MediaFile
This opens the file and creates an alias named MediaFile which can be used to refer to the file for all future commands. Send the above command using the mciSendString method would look as follows:
string cmd = "open \"" + _musicFile + "\" type mpegvideo alias MediaFile";
mciSendString(cmd, null, 0, IntPtr.Zero);
Dim cmd As String = "open """ + file + """ type mpegvideo alias MediaFile"
mciSendString(cmd, Nothing, 0, IntPtr.Zero)
The remaining commands we will need are:
Next, the Phidget boards behavior needed to be implemented. Talking to the Phidget board is very easy. After creating an instance of the InterfaceKit object, a specific device can be opened by calling the open method, passing in the serial number of the device to be opened.
The serial numbers for each attached Phidget device can be determined by creating an instance of the Phidgets.Manager class, setting up the Attach event handler, and listening for the attach events as follows:
Phidgets.Manager phidgetsManager = new Phidgets.Manager();
phidgetsManager.Attach += new AttachEventHandler(phidgetsManager_Attach);
void phidgetsManager_Attach(object sender, AttachEventArgs e)
Debug.WriteLine(e.Device.Name + " - " + e.Device.SerialNumber)
Dim phidgetsManager as New Phidgets.Manager
AddHandler phidgetsManager.Attach, AddressOf Me.phidgetsManager_Attach
Private Sub phidgetsManager_Attach(ByVal sender As Object, ByVal e As AttachEventArgs)
Debug.WriteLine(e.Device.Name & " - " & e.device.SerialNumber)
Setting the relay state is as easy as indexing into the outputs array of the InterfaceKit object and setting the indexed output to true or false.
In code, all of this would look like:
InterfaceKit ik = new InterfaceKit();
ik.outputs = true;
Dim ik as New InterfaceKit
ik.outputs(0) = True
In order to maintain precise timing, the Stopwatch class from the System.Diagnostics namespace is used. This internally uses the QueryPerformanceCounter Win32 API method to give extremely precise time values.
When it is time to playback a sequence, a thread is started which starts the music using the appropriate playback engine, and then sits in a loop, waiting for the number of milliseconds to pass specified in the sequence (50ms as default). When that amount of time has elapsed, we send the channel states of the current tick to the relays connected.
Recording a sequence with the keyboard works in a similar fashion. A thread is started and the music is played. While the music is playing, KeyDown and KeyUp events are listened for. After translating the KeyCode of the pressed key to the channel number, an internal array of which keys are "on and off" are maintained. When the number of milliseconds elapsed hits the appropriate mark, every channel is updated with the current value of that array. That is, which keys are up and down.
When the user stops playback, or the song ends, the channel data is returned to the main form and displayed in the main grid.
If a MIDI file is selected, sequence data is automatically generated based on the MIDI data. A MIDI file contains a series of tracks or channels with a series of commands. These commands tell the MIDI hardware what note to turn on, when, and for how long (among other things). When the MIDI file is loaded, every command from every channel is enumerated and its time values are converted into milliseconds. Once all the commands are gathered and organized, they are placed into this application's channel structure and displayed on the grid. This allows the lights to flash in precise time to the MIDI file being played.
Using the Software
Ensure that the Phidget devices you will be using are attached to the PC. Start by creating a new sequence from the File menu or by clicking the New Sequence button. In the dialog that appears, locate a music file to play back and enter the length of time that the sequence should run. Be sure to note which Phidget devices are attached and which channels they map to on the grid. Click OK when complete.
The screen will redraw and present the grid interface for the length of time specified. At this point, cells can be turned on and off by highlighting a cell and right-clicking, or by pressing the "O" key to turn the cell On, and the "F" key to turn the cell off. Multiple cells can be selected and changed at once.
To use the recording interface, click the Record Sequence button or choose Record Sequence from the Sequence menu. Be sure to select the correct choice of "Overwrite channel data" or "Append channel data." As you record additional channels, you will almost always want to append and not overwrite.
Click the start button and a brief countdown will begin. When the countdown reaches 0, the music will begin. A channel can be recorded by pressing the keyboard key of the channel number. For example, to tap out the rhythm of channel 1, press the 1 key at the appropriate times.
When complete, press the Escape key, or click the Stop button. When the Record window is closed, the main grid will be updated with the sequence recorded.
Creating a sequence is certainly a time consuming task since each channel needs to be recorded. While the rhythm interface allows one to record many channels simultaneously, I think it would be impossible for anyone to type out an entire sequence for all channels in one go. In my opinion, it is easiest to record one or two channels at a time and append the data as you go. In the end, you can use the grid interface to tweak the values and clean up any mistakes.
The sequence can be played back at any time. Simply press the Play button and watch your holiday lights play back to the timing you created. Press the Stop button to end the current playback.
Sequences can be saved at any time by selecting Save from the file menu.
To test the channels by hand, select "Test Channels" from the "Tools" menu. As with the recording screen, press the number keys associated with the channel to turn on or off to test that channel.
I found it was much easier to also have the lights plugged into the appropriate channels as I created my sequence. That way I could see the results of my recordings immediately.
To create a playlist of many sequences, select New Playlist from the File menu. Add your existing sequence files, order them as you wish, and save the playlist. From this screen you can also play the playlist, set it up to repeat, advance tracks, etc.
To edit data on an existing sequence (Phidget serial numbers, mapped MIDI channels, etc.) select Edit Sequence Properties from the Sequence menu. This will display the New Sequence dialog box and allow you to edit the existing setup.
So now that you have hardware, music and an animated sequence, it's time to hook it all up! If you are doing an indoor show, a set of external speakers should be more than ample for playing the music for your light show. For an outdoor show, you may wish to purchase an FM transmitter to output the music over a very low-powered FM frequency so that visitors can listen to the music on their car radios. I personally have not gone this route, however you can find a variety of FM transmitters for sale around the 'net. In a quick search, I found Ramsey Electronics which sells a variety of FM transmission hardware that is more than appropriate depending on your budget. I am certain there are plenty of other devices that fit the bill as well.
To fire up the show, just plug in your USB Phidget devices, plug the lights into the appropriate channels, load the Light Sequencer application, and press the Play button!
And there we have it! Holiday lights timed to your favorite holiday song. Take your time in creating a sequence and show us what you've created!
I plan on maintaining and updating this article as we get closer and closer to the holidays, so please check back often for updates. I will note updates at the top of the article. Additionally, please send me any and all feedback, bug reports, feature requests, or anything else you have to say! You can find my contact info in the readme.txt file located in the source code download linked above, or visit my website.
Special thanks to Michelle Leavitt for help setting up lights and sequence ideas, and my dad for advice on wiring up the relays.
And, a big thank you to the beta testers for version 2: Allen Leno, Steve Runion, Steve Trueman, Corey Emmert.
Brian is a Microsoft C# MVP and a recognized .NET expert with over 6 years experience developing .NET solutions, and over 9 years of professional experience architecting and developing solutions using Microsoft technologies and platforms, although he has been "coding for fun" for as long as he can remember. Outside the world of .NET and business applications, Brian enjoys developing both hardware and software projects in the areas of gaming, robotics, and whatever else strikes his fancy for the next ten minutes. He rarely passes up an opportunity to dive into a C/C++ or assembly language project. You can reach Brian via his blog at http://www.brianpeek.com/.