General guidance needed on timed stage light project

I am looking at programming my Arduino to control a number of light effects and smoke machines (via an 8 relay module) to align with changes in a musical score. Timing does not need to be precise but close (like one tune ends and a spot light turns on and the smoke stops). This program only needs to be executed once.
I know the exact timing of the music (complex tunes by Stevie Ray Vaughan) and where changes are required.
My questions are
a) Do I use the 'Delay' function for setting timing or is there a better way?
b) How do I start and then stop the program so it executes only once?
c) Any other suggestions?

Thank You

Maybe. It is the simplest approach, but there are drawbacks:

  1. The Arduino can do nothing else while executing the delay(). Nothing.
  2. The timing may need careful adjusting because the time to execute the commands between the delays does not take zero time.

But from what you have described so far, I don't think these will be problems for you.

Put all the code in setup() and leave loop() empty.

Usually stage lighting and effects are DMX512 controlled. There are manual controllers and computer-automated controllers.

You can go "half way" and build a DMX controller with an Arduino but I wouldn't recommend it. The only reason I'd build something myself would be if there is some effect that's not available so it has to be custom. And in that case I'd still make it work with a regular DMX controller.

There are advantages to using an off-the-shelf configuration rather than something "homemade". It's faster than building from scratch, it's usually more reliable (super important in live shows), and if it breaks you don't have to rely on the designer/builder to get the show back up-and-running.

A quick search found this.

CONFESSION - I have made several Arduino-based sound-activated lighting effects which are not DMX but they are for "personal hobby use", and I've used them a couple of times for DJ gigs.

Appreciate that DMX Controller might be a better solution but the cost is more than I want to spend as I already have the Arduino hardware and this is a one off Halloween Pantomime. Perhaps in the future. Thank You

Thank You for the reply. Basically I was just thinking of a Delay-Activate a relay-delay-Activate another relay-Delay-Deactivate the first relay etc As long as the time to execute commands is reasonably the same each time I can adjust the timing to match the music.

yes a very simple code should get you going

The code will wait for a button (on pin 10) to be pressed to actually start the animation

constexpr uint8_t light1 = 2;
constexpr uint8_t light2 = 3;
constexpr uint8_t light3 = 4;
constexpr uint8_t light4 = 5;
constexpr uint8_t light5 = 6;
constexpr uint8_t light6 = 7;
constexpr uint8_t light7 = 8;
constexpr uint8_t smoke  = 9;
constexpr uint8_t button = 10;

void setup() {
  pinMode(light1, OUTPUT);
  pinMode(light2, OUTPUT);
  pinMode(light3, OUTPUT);
  pinMode(light4, OUTPUT);
  pinMode(light5, OUTPUT);
  pinMode(light6, OUTPUT);
  pinMode(light7, OUTPUT);
  pinMode(smoke,  OUTPUT);
  pinMode(button, INPUT_PULLUP);

  while (digitalRead(button) != LOW) yield(); // wait for button press

  // animation example 
  digitalWrite(smoke, HIGH);
  digitalWrite(light1, HIGH);
  delay(5000);

  digitalWrite(light2, HIGH);
  delay(3000);

  digitalWrite(light1, LOW);
  digitalWrite(light3, HIGH);
  delay(4000);

  digitalWrite(smoke, LOW);
  digitalWrite(light4, HIGH);
  delay(2000);

  digitalWrite(light2, LOW);
  digitalWrite(light5, HIGH);
  delay(3000);

  digitalWrite(light3, LOW);
  digitalWrite(light6, HIGH);
  delay(3000);

  digitalWrite(light4, LOW);
  digitalWrite(light7, HIGH);
  delay(5000);

  digitalWrite(light5, LOW);
  digitalWrite(light6, LOW);
  digitalWrite(light7, LOW);
}

Note that this is a lot of repetition and boring to maintain

since you have only 8 elements to manage and that a byte is 8 bits, you could represent the status of your 8 devices as a byte and then add how long it should be in that state in a structure. A sequence would be an array of that structure and then a simple for loop could call a function going through the sequence and setting the states.

constexpr uint8_t relays[8] = {
  2,  // light0 → bit 0
  3,  // light1 → bit 1
  4,  // light2 → bit 2
  5,  // light3 → bit 3
  6,  // light4 → bit 4
  7,  // light5 → bit 5
  8,  // light6 → bit 6
  9   // smoke  → bit 7
};

constexpr uint8_t button = 10;

struct State {
  byte status;
  uint16_t duration;
};

constexpr State sequence[] = {
  {0b10000001, 5000},  // smoke + light0 on
  {0b10000011, 3000},  // add light1
  {0b10000110, 4000},  // light0 off, light2 on
  {0b00001100, 2000},  // smoke off, light3 on
  {0b00010000, 3000},  // light1 off, light4 on
  {0b00100000, 3000},  // light2 off, light5 on
  {0b01000000, 5000},  // light3 off, light6 on
  {0b00000000, 1},     // all off
};

void setOutputsAndWait(byte status, uint16_t duration) {
  for (uint8_t i = 0; i < 8; ++i)
    digitalWrite(relays[i], bitRead(status, i) ? HIGH : LOW); // or LOW : HIGH depending on how your relay works
  delay(duration);
}

void setup() {
  for (uint8_t pin : relays) pinMode(pin, OUTPUT);
  pinMode(button, INPUT_PULLUP);

  while (digitalRead(button) != LOW);

  for (const auto &state : sequence)
    setOutputsAndWait(state.status, state.duration);
}

void loop() {}

if you are on a UNO or NANO and you map the relays to a PORT, instead of the for loop with the digitalWrite() in the function, you could directly write the port to instantly set the status of the 8 devices.

Modifying the sequence now is just a matter of editing the array.

Side note:
If the last step is always shutting everything off, you could hardwire that at the end of the setup so that you don't have to add a last entry in the array that says

  {0b00000000, 1},  // all off

I normally use a Sequencer loop for this.
Imagine you have an array of timestamps each representing when something must happen.
The program loops through them comparing millis() to the current timestamp. When its equal to or greater than that time, do the action and move on to the next array item.

Pretty easy and as long as your actions complete in time, you dont need to worry about delays accumulating.