During my time at Six Flags, I set out to create a lightning effect on the ruins of a building located within a haunted trail. In previous years, we used a series of strobe lights set at random rates to achieve the effect, but the goal was to better synchronize the lightning with a thunder sound effect. The resulting effect can be seen in this video, though the audio is hard to make out:
How does it work? Unfortunately I was unable to get photos of the unit, though I should soon in the off-season, but the system is quite simple. Originally, the plan was to use solid state relays, due to their ability to quickly switch on and off – meaning that using PWM signals, we could achieve a form of dimming. This was crucial to the intended effect, because we wanted a direct correlation between the volume of the thunder, and the brightness of the lightning flashes. However, after doing some digging, it turned out that solid state relays can give the appearance of dimming, but in order to truly have dimmer control we would need to utilize a TRIAC board with a zero cross detection signal. Essentially, AC current in the U.S. goes to zero volts 120 times per second, making for a time of about 8.3ms between zero-crossings. A TRIAC can turn on at any point after the zero-crossing, but cannot be turned off until the next zero cross – thus, to achieve dimming, you must delay the time between the zero crossing and switching on the TRIAC to effectively control what percentage of the AC wave is passed to the light.
This is all a very surface level explanation, as I am not an electrical engineer, but the principles make sense. If you give the light 70% of the wave, you’ll achieve roughly 70% brightness. Of course since the wave is not a square signal but sinusoidal, it is not quite that linear, but for our purposes this was a fair approximation. The next problem was figuring out how to play an audio file, while analyzing it and turning certain frequencies or tones into a lightning effect, which would mean it needs to accommodate an interrupt at 60Hz (every 8.3ms, it would need to run a quick routine to update brightness levels). As I had never worked with one before, but heard they supported audio quite well, I elected to try out a Teensy 3.6 micro controller. The audio library as well as the massive amount of pins, built in microSD slot, and 32 bit architecture were main selling points. I also, as a self-proclaimed electronics geek/nerd, really wanted to try one out.
Little did I know, that 32-bit architecture meant a complete incompatibility with the TRIAC control libraries I had planned to utilize. So I was faced with either trying to modify an existing library, or writing my own rudimentary program that included the library functionality. How hard could it be? Well, as it turns out, not too bad. I found an old github which had used a Teensy as a DMX dimmer controller, and was able to learn a lot from it. While the controls would be very different, the interrupt routines and zero cross detection would be practically identical. While I cannot locate that Github today, there are numerous similar forum posts on the PJRC forums which in hindsight provide even cleaner means of accomplishing what I needed! But alas, here is the code that I ended up with:
#include <Audio.h> #include <Wire.h> #include <SPI.h> #include <SD.h> #include <SerialFlash.h> // GUItool: begin automatically generated code AudioPlaySdWav playSdWav1; //xy=208,279 AudioAnalyzeToneDetect tone2; //xy=460,424 AudioAnalyzeToneDetect tone3; //xy=461,458 AudioAnalyzeToneDetect tone4; //xy=461,495 AudioAnalyzeToneDetect tone1; //xy=462,387 AudioAnalyzeToneDetect tone5; //xy=462,531 AudioOutputAnalogStereo dacs1; //xy=464,279 AudioAnalyzeToneDetect tone6; //xy=463,571 AudioConnection patchCord1(playSdWav1, 0, dacs1, 0); AudioConnection patchCord2(playSdWav1, 0, tone1, 0); AudioConnection patchCord3(playSdWav1, 0, tone2, 0); AudioConnection patchCord4(playSdWav1, 0, tone3, 0); AudioConnection patchCord5(playSdWav1, 0, tone4, 0); AudioConnection patchCord6(playSdWav1, 0, tone5, 0); AudioConnection patchCord7(playSdWav1, 0, tone6, 0); AudioConnection patchCord8(playSdWav1, 1, dacs1, 1); // GUItool: end automatically generated code // Use these with the Teensy 3.5 & 3.6 SD card #define SDCARD_CS_PIN BUILTIN_SDCARD #define SDCARD_MOSI_PIN 11 // not actually used #define SDCARD_SCK_PIN 13 // not actually used //#define DEBUG //define things #define timer0_duration 30 #define NUM_DIMMERS 6 #define NUM_DIMMERS_USED 6 //set relay pins! int zeroCrossPin = 2; int fadeValues[NUM_DIMMERS]; const int syncPin = 2; const int dim1 = 3; const int dim2 = 4; const int dim3 = 5; const int dim4 = 6; const int dim5 = 7; const int dim6 = 8; char audioFiles[100]; int numberFiles; int DIMMERS[NUM_DIMMERS] = {dim1,dim2,dim3,dim4,dim5,dim6}; elapsedMicros sinceInterrupt; unsigned long thePeriod = 0; volatile unsigned int timerFire_cnt = 0; IntervalTimer timer0; void setup() { Serial.begin(9600); //-----------------------TRIAC Control--------------------- pinMode(zeroCrossPin, INPUT); attachInterrupt(zeroCrossPin, zero_crosss_int, RISING); timer0.begin(timerFire, timer0_duration); for(int i=0; i <NUM_DIMMERS_USED;i++){ pinMode(DIMMERS[i], OUTPUT); // Set the AC Load as output } //--------------------------------------------------------- AudioMemory(12); Serial.println("Done!"); //set frequencies to be analyzed tone1.frequency(40); tone2.frequency(65); tone3.frequency(90); tone4.frequency(115); tone5.frequency(150); tone6.frequency(190); //Connect to the SD card SPI.setMOSI(SDCARD_MOSI_PIN); SPI.setSCK(SDCARD_SCK_PIN); if (!(SD.begin(SDCARD_CS_PIN))) { // stop here, but print a message repetitively while (1) { Serial.println("Unable to access the SD card"); delay(500); } } } double mapf(double x, double in_min, double in_max, double out_min, double out_max) { return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; } void printDirectory(File dir, int numTabs) { for (int i = 0; i<100; i++) { File entry = dir.openNextFile(); if (! entry) { // no more files break; } for (uint8_t i = 0; i < numTabs; i++) { Serial.print('\t'); } Serial.print(entry.name()); audioFiles[i] = entry.name(); numberFiles = i; if (entry.isDirectory()) { Serial.println("/"); printDirectory(entry, numTabs + 1); } else { // files have sizes, directories do not Serial.print("\t\t"); Serial.println(entry.size(), DEC); } entry.close(); } } void playFile(const char *filename) { //Serial.print("Playing file: "); //Serial.println(filename); // Start playing the file. This sketch continues to // run while the file plays. playSdWav1.play(filename); // A brief delay for the library read WAV info while (playSdWav1.isPlaying() == false){ //wait for it to start! } // Run Lightning! while (playSdWav1.isPlaying()) { double ch1, ch2, ch3, ch4, ch5, ch6, val, min_level; ch1 = tone1.read(); ch2 = tone2.read(); ch3 = tone3.read(); ch4 = tone4.read(); ch5 = tone5.read(); ch6 = tone6.read(); //Serial.println(AudioMemoryUsageMax()); //Serial.println(AudioProcessorUsageMax()); min_level = 1; double thunder[] = {ch1, ch2, ch3, ch4, ch5, ch6}; for (byte i = 0; i < (sizeof(thunder) / sizeof(thunder[0])); i++) { val = thunder[i]*100; //Serial.print(String(val)+", "); if (val > min_level){ min_level = val; } } //Serial.println(min_level); for (byte i = 0; i < (sizeof(thunder) / sizeof(thunder[0])); i++) { fadeValues[i] = mapf(thunder[i]*100,0,min_level,8333,0); Serial.print(fadeValues[i]); } Serial.println(); //delay(50); } //This is where a reset could occur after? } void rotate(int arr[], int n) { int x = arr[n - 1], i; for (i = n - 1; i > 0; i--) arr[i] = arr[i - 1]; arr[0] = x; } void zero_crosss_int(){ // Ignore spuriously short interrupts if (sinceInterrupt < 2000){ #ifdef DEBUG Serial.println("sinceInterrupt < 2000 "); #endif return; } thePeriod = sinceInterrupt; //ca. 10012 at 50Hz location, 8300 at 60 hz in montreal sinceInterrupt = 0; #ifdef DEBUG Serial.print("timerFire_cnt "); Serial.println(timerFire_cnt); timerFire_cnt = 0; #endif // Serial.print("----------thePeriod----------"); // Serial.println(thePeriod); for(int i=0; i<NUM_DIMMERS_USED;i++){ digitalWrite(DIMMERS[i], LOW); } } void timerFire(void){ for(int i=0; i<NUM_DIMMERS_USED;i++){ if(sinceInterrupt >= fadeValues[i] ){ // if(temp_sinceInterrupt >= fadeValues[i] ){ //for first part of 1/2 sinus curve triac is off, then we turn triac on by setting pin high //triac automatically turns on next zero crossing //but not arduino pin digitalWrite(DIMMERS[i], HIGH); } } } void loop() { rotate(DIMMERS, 1); //Serial.println("Still Running: " + String(millis())); //int file2play = random(0,numberFiles); playFile("0001.WAV"); }
While it can definitely be optimized further, and some deprecated parts removed, it worked beautifully! With six lights connected, each reacting to a different frequency, the effect was better than I expected. By shifting lights by 1 after every cycle (output 1 became 2, 2 became 3 … and 6 became 1) the effect had a loop length that exceeded the operational window of the attraction before it would ever have an identical effect. Some changes I would make in the future are swapping to cool white lights, and adding more thunder files. I tried adding more with a random selection, but it failed and time did not allow for further investigation so it was quickly commented out. But, for something that was ultimately thrown together in a matter of days, and then stuck out in the woods for the season, it worked surprisingly well. I think my favorite part of using micro-controllers is how fast they go from off to functioning. The lightning box, as the toolbox this system lives in was named, starts doing its thing within seconds of receiving power!
One detail I did not mention yet is the audio out. To keep it simple, I soldered a stereo headphone jack to the ADC of the Teensy, but with a small capacitor inline to block any DC signal from making its way out to the jack. From there, it plugs into a powered speaker (it’s a line-level out, so an amplifier is needed!) and the effect is ready. The lights plug into a bank of outlets that are connected to the TRIAC board inside the box using lamp cord. For a temporary installation, this is sufficient. For a permanent one, a lower (heavier) gauge and fuses would have been ideal. Since each light was an LED PAR38, the current draw is extremely low (less than 1A per light), well within the 2A/5A Peak capabilities of the TRIACs, and within the 20A feed the unit takes (in the event all lights go to full).