Built-in project · Arduino Uno R3

Christmas light show on an Arduino Uno

A DFPlayer starts the song and three WS2812B strips, roofline, tree and arch, follow a cue list timed in beats from that moment until the song ends. The whole build, an Arduino Uno and 9 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.

Teaching? Assign it to your class in one click.

Advanced Runs in your browser. Free, and no account needed.

Build it as the Christmas Lights Kit: the parts in numbered bags and a booklet that checks every step. Free.

Christmas light show · Arduino Uno R3live0.000 s 0.00x
Click to open it in the editor
The circuit itself, running here on the simulator. Press what can be pressed; click anything else to open it in the editor.

The code

The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.

sketch.ino · Arduino Uno R3
// Christmas light show: a song plays from a DFPlayer Mini and three LED
// strips, the roofline, the tree and the arch, change in time with it,
// following a cue list written in beats.
//
//   DFPlayer   VCC -> 5 V   GND -> GND   TX -> pin 10   RX <- 1 k <- pin 11
//              BUSY -> pin 3 (low while a track plays), speaker on SPK_1/SPK_2
//              track 001.mp3 is the song
//   roofline   16 WS2812B, DIN <- 330 ohms <- pin 5
//   tree       12 WS2812B, DIN <- 330 ohms <- pin 6
//   arch        8 WS2812B, DIN <- 330 ohms <- pin 7
//
// How the sync works. The lights do not listen to the music. The sketch
// starts the track and its own clock at the same moment, and from then on
// works out which beat of the song it is from the clock alone:
//
//   beat = (millis() - songStart) / BEAT_MS
//
// A cue list says what look the strips show from which beat on. That is the
// same idea big shows use, at a much smaller size. In xLights, a free
// sequencer for PCs, you lay effects out on a timeline against the song's
// waveform, export the result as frames, and a show player streams those
// frames to pixel controllers over the network while it plays the audio.
// Mokxi is not xLights and does not run its sequences: this sketch is the
// hand-written version, for a few strips on one Uno.
//
// The other way is beat detection: a microphone module and a sketch that
// flashes on every loud moment. It works with any song and needs no
// sequencing, but it reacts rather than performs, so nothing can happen just
// before the drop.
//
// Getting the timing right. Measure your song's tempo (many free tools tap or
// detect BPM) and set BEAT_MS = 60000 / BPM. The DFPlayer takes a moment to
// start a track, so START_DELAY_MS shifts the lights to match. BUSY tells the
// sketch when the song has really ended, and the show starts over after a
// short dark pause.
//
// The simulated DFPlayer plays silence. It shows the track and the seconds
// into it, so the beat count and the player's clock can be compared on the
// canvas; the song itself is for the speaker on the porch.
//
// On the porch: LED strips draw far more than the Uno can supply. Give them a
// 5 V supply sized for the LEDs you run, inject power at both ends of long
// strips, join every ground, and keep the supply's mains side in its case and
// out of the weather.
//
// Things to try. Add a cue. Change BEAT_MS for your song. Write a new look in
// render() and give it a beat.

#include <SoftwareSerial.h>
#include <DFRobotDFPlayerMini.h>
#include <Adafruit_NeoPixel.h>

const int BUSY_PIN = 3;
const unsigned long BEAT_MS = 500;          // 120 beats per minute
const unsigned long START_DELAY_MS = 150;   // the player's head start
const int SONG_BEATS = 24;                  // used only without a player
const unsigned long DARK_MS = 4000;         // pause between shows
const unsigned long FRAME_MS = 20;          // fifty frames a second is plenty

Adafruit_NeoPixel roof(16, 5, NEO_GRB + NEO_KHZ800);
Adafruit_NeoPixel tree(12, 6, NEO_GRB + NEO_KHZ800);
Adafruit_NeoPixel arch(8, 7, NEO_GRB + NEO_KHZ800);

SoftwareSerial link(10, 11);   // RX, TX
DFRobotDFPlayerMini player;
bool haveSound = false;

enum Look { INTRO, CHASE, PULSE, WIPE, RAINBOW, FINALE, DARK };
const char *const NAMES[] = {"intro", "chase", "pulse", "wipe", "rainbow", "finale", "dark"};

struct Cue {
  uint8_t beat;   // from this beat of the song...
  uint8_t look;   // ...show this look
};

// The sequence, in beats. Four beats is one bar.
const Cue CUES[] = {
  {0, INTRO}, {4, CHASE}, {8, PULSE}, {12, WIPE}, {16, RAINBOW}, {20, FINALE}, {24, DARK},
};
const int CUE_COUNT = sizeof(CUES) / sizeof(CUES[0]);

bool playing = false;
unsigned long songStart = 0;
unsigned long darkSince = 0;
unsigned long nextFrame = 0;
int lastCue = -1;

uint32_t rgb(uint8_t r, uint8_t g, uint8_t b) { return Adafruit_NeoPixel::Color(r, g, b); }

// Draw one frame of a look. `beat` is the whole beat, `phase` how far into
// it we are, 0 to 255, which is what makes a look move with the music.
void render(int look, int beat, int phase) {
  int fade = 255 - phase;   // bright on the beat, dimming until the next
  switch (look) {
    case INTRO:
      tree.fill(rgb(beat * 50 + phase / 6, beat * 40 + phase / 8, beat * 20));
      roof.clear();
      arch.clear();
      break;
    case CHASE:
      for (int i = 0; i < 16; i++) roof.setPixelColor(i, (i + beat) % 2 ? rgb(200, 0, 0) : rgb(0, 160, 0));
      tree.fill(rgb(0, 120, 0));
      arch.clear();
      break;
    case PULSE:
      roof.fill(rgb(fade, fade, fade));
      tree.fill(rgb(fade, fade * 3 / 4, fade / 3));
      arch.fill(rgb(fade, fade, fade));
      break;
    case WIPE:
      arch.clear();
      for (int i = 0; i <= phase * 8 / 256; i++) arch.setPixelColor(i, rgb(200, 0, 0));
      for (int i = 0; i < 16; i++) roof.setPixelColor(i, (i / 4 + beat) % 2 ? rgb(180, 180, 180) : rgb(180, 0, 0));
      tree.fill(rgb(0, 100, 0));
      tree.setPixelColor(random(0, 12), rgb(255, 255, 200));
      break;
    case RAINBOW: {
      uint16_t hue = (uint16_t)(beat * 16384 + phase * 64);
      roof.rainbow(hue);
      tree.rainbow(hue + 21845);
      arch.rainbow(hue + 43690);
      break;
    }
    case FINALE: {
      bool red = (phase < 128) != (beat % 2);
      uint32_t a = red ? rgb(255, 0, 0) : rgb(0, 200, 0);
      uint32_t b = red ? rgb(0, 200, 0) : rgb(255, 0, 0);
      roof.fill(a);
      tree.fill(b);
      arch.fill(a);
      break;
    }
    default:
      roof.clear();
      tree.clear();
      arch.clear();
      break;
  }
  roof.show();
  tree.show();
  arch.show();
}

void startShow() {
  if (haveSound) player.play(1);
  songStart = millis() + START_DELAY_MS;
  playing = true;
  lastCue = -1;
  Serial.println("Show starting");
}

void setup() {
  Serial.begin(9600);
  link.begin(9600);
  pinMode(BUSY_PIN, INPUT);
  roof.begin();
  tree.begin();
  arch.begin();
  roof.setBrightness(150);
  tree.setBrightness(150);
  arch.setBrightness(150);
  haveSound = player.begin(link);
  if (haveSound) {
    player.volume(25);
  } else {
    Serial.println("No DFPlayer answered: the show will run silent");
  }
  startShow();
}

void loop() {
  unsigned long now = millis();

  if (!playing) {
    if (now - darkSince >= DARK_MS) startShow();
    return;
  }
  if (now < songStart || now < nextFrame) return;
  nextFrame = now + FRAME_MS;

  unsigned long t = now - songStart;
  int beat = t / BEAT_MS;
  int phase = (t % BEAT_MS) * 256 / BEAT_MS;

  // The last cue whose beat has come.
  int cue = 0;
  while (cue + 1 < CUE_COUNT && beat >= CUES[cue + 1].beat) cue++;
  if (cue != lastCue) {
    lastCue = cue;
    Serial.print("Beat ");
    Serial.print(beat);
    Serial.print(": ");
    Serial.println(NAMES[CUES[cue].look]);
  }
  render(CUES[cue].look, beat, phase);

  // The song is over when the player says so (BUSY high), or, with no
  // player, when the beats run out.
  bool songOver = haveSound ? (t > 1000 && digitalRead(BUSY_PIN) == HIGH) : beat >= SONG_BEATS;
  if (songOver) {
    render(DARK, 0, 0);
    playing = false;
    darkSince = now;
    Serial.println("Show over");
  }
}

Parts list

11 parts, plus the jumper wires. Every one is in the editor's parts bin.

How it is wired

14 connections, pin by pin, read from the circuit itself. Each line is one set of pins joined together, by a jumper wire or a breadboard strip.

  • Arduino Uno R3 pin 11; Resistor, 1k Ω pin 1
  • Arduino Uno R3 pin 10; DFPlayer Mini MP3 module pin TX
  • Arduino Uno R3 pin 7; Resistor, 330 Ω (3) pin 1
  • Arduino Uno R3 pin 6; Resistor, 330 Ω (2) pin 1
  • Arduino Uno R3 pin 5; Resistor, 330 Ω (1) pin 1
  • Arduino Uno R3 pin 3; DFPlayer Mini MP3 module pin BUSY
  • Arduino Uno R3 pin 5V; DFPlayer Mini MP3 module pin VCC; Addressable RGB LEDs (1) pin VDD; Addressable RGB LEDs (2) pin VDD; Addressable RGB LEDs (3) pin VDD
  • Ground: Arduino Uno R3 pin GND; DFPlayer Mini MP3 module pin GND; Addressable RGB LEDs (1) pin VSS; Addressable RGB LEDs (2) pin VSS; Addressable RGB LEDs (3) pin VSS
  • DFPlayer Mini MP3 module pin RX; Resistor, 1k Ω pin 2
  • DFPlayer Mini MP3 module pin SPK_1; Speaker, 8 ohm pin 1
  • DFPlayer Mini MP3 module pin SPK_2; Speaker, 8 ohm pin 2
  • Resistor, 330 Ω (1) pin 2; Addressable RGB LEDs (1) pin DIN
  • Resistor, 330 Ω (2) pin 2; Addressable RGB LEDs (2) pin DIN
  • Resistor, 330 Ω (3) pin 2; Addressable RGB LEDs (3) pin DIN

Change it and keep it

Open it in the editor, change the circuit or the code, and keep your version in a free account.