RobSmithDev
Published © CC BY-NC-SA

Modding a Pac-Man Retro Light with Arduino

I'm modding nice Retro Pacman Light with an Arduino Nano to improve its functionality and effects including sound to light.

IntermediateWork in progress1 hour1,406
Modding a Pac-Man Retro Light with Arduino

Things used in this project

Hardware components

Arduino Nano R3
Arduino Nano R3
×1
Retro Pacman Light
×1

Software apps and online services

Arduino IDE
Arduino IDE

Story

Read more

Schematics

Final Circuit Diagram

Code

Final Code

C/C++
// New Pacman Light Controller
// Created by RobSmithDev
// https://robsmithdev.co.uk
// https://www.youtube.com/c/RobSmithDev

#include <EEPROM.h>
#define FHT_N 256
#define LOG_OUT 1
#include <FHT.h>

#define PIN_GHOST_RED     3
#define PIN_GHOST_ORANGE  5
#define PIN_GHOST_BLUE    6
#define PIN_PACMAN        9

#define PIN_MODE_BUTTON   2

#define PIN_AUDIO_IN      A0

// Number of modes we support
#define MAX_MODES         7

enum Modes { mOff = 0, mOn = 1, mColourPhasing = 2, mPartyMode = 3, mVUMeter = 4, mRealVUMeter=5, mFrequencyMode=6 };

// A variable to store the current mode
volatile Modes mode = Modes::mOff;
volatile bool modeChanged = false;

// When the last loop occured, and the last time the mode was changed
unsigned long lastModeChange;

// Simple flag if audio is recording
bool isAudioRecording = false;

// We will record 256 samples and then take an average
unsigned char sampleCount = 0;
unsigned long totalCount;
unsigned short dcBiasLevel = 320;
unsigned short averageValue = 0;
unsigned short loudestValue = 0;
unsigned long lastCalculated;
volatile bool newSampleGroup = false;
unsigned char ghostPWMValue = 0;
unsigned char ghostPWMValueCounter = 0;

// Recorded samples
unsigned short samples[256];

// This function will execute then the button is pressed in.
void buttonPressInterruptHandler() {
  // You can use millis in an interrupt, but you cant stay in it too long.  
  // If this was pressed too recently, ignore it. Probably switch bouncing
  if (millis() - lastModeChange<450) return;
  lastModeChange = millis();

  // Go to the next mode
  modeChanged = true;
  mode = (Modes)((mode + 1) % MAX_MODES);
  EEPROM.put(0,(unsigned char)mode);
}

// Turns off everything
void turnOffEverything() {
  disableAudioRecording();
  analogWrite(PIN_GHOST_RED,0);
  analogWrite(PIN_GHOST_ORANGE,0);
  analogWrite(PIN_GHOST_BLUE,0);
  analogWrite(PIN_PACMAN,0);
  digitalWrite(PIN_GHOST_RED,LOW);
  digitalWrite(PIN_GHOST_ORANGE,LOW);
  digitalWrite(PIN_GHOST_BLUE,LOW);
  digitalWrite(PIN_PACMAN,LOW);  
}

void setup() {
  Serial.begin(9600);       // Start serial communication
  
  // put your setup code here, to run once:
  pinMode(PIN_GHOST_RED, OUTPUT);
  pinMode(PIN_GHOST_ORANGE, OUTPUT);
  pinMode(PIN_GHOST_BLUE, OUTPUT);
  pinMode(PIN_PACMAN, OUTPUT);

  pinMode(PIN_MODE_BUTTON, INPUT_PULLUP);
  pinMode(PIN_AUDIO_IN, INPUT);

  turnOffEverything();
  lastModeChange = millis();

  // Get the last mode we were on from EEPROM
  unsigned char lastMode;
  EEPROM.get(0, lastMode);
  mode = (Modes)lastMode;
  modeChanged = true;  
  attachInterrupt(digitalPinToInterrupt(PIN_MODE_BUTTON), buttonPressInterruptHandler, FALLING);
}

// Interrupt routine, that will run at 8khz for audio sampling
ISR(TIMER2_COMPA_vect) {
  // Capture and store
  unsigned int soundLevel = analogRead(PIN_AUDIO_IN);
  samples[sampleCount] = soundLevel;

  // Keep runnign total
  totalCount+=soundLevel;
  sampleCount++;

  if (sampleCount == 255) {
    // Maintain average (DC bias)
    dcBiasLevel = ((dcBiasLevel * 4) + (totalCount/256))  / 5; 
    sampleCount = 0;    
    totalCount = 0;
    loudestValue = 0;
    unsigned int total = 0;

    // Now look at the samples and see what was the loudest    
    for (unsigned char i=0; i<255; i++) {
      unsigned short volume;
      if (samples[i] < dcBiasLevel) 
        volume = dcBiasLevel - samples[i]; 
      else volume = samples[i] - dcBiasLevel;
      total += volume;    
    }

    loudestValue = (loudestValue + (total / 256)) / 2;
        
    // Continue to improve our average volume
    averageValue = ((averageValue * 10) + loudestValue) / 11;
    newSampleGroup = true;
  }
  lastCalculated = millis();

  // Manual PWM for the ghost seeing as we're using its PWM channel
  if (mode == Modes::mFrequencyMode) {
    ghostPWMValueCounter+=8;
    digitalWrite(PIN_GHOST_RED, ghostPWMValue>ghostPWMValueCounter);
  }
}

// Enable audio recording at 8khz.  This will prevent PWM working on the RED ghost, but we wont be using PWM
// See http://www.8bit-era.cz/arduino-timer-interrupts-calculator.html
void enableAudioRecording() {
  if (isAudioRecording) return;
  isAudioRecording = true;

  // Stop interrupts
  cli();
  TCCR2A = 0; // set entire TCCR2A register to 0
  TCCR2B = 0; // same for TCCR2B
  TCNT2  = 0; // initialize counter value to 0
  // set compare match register for 8000 Hz increments
  OCR2A = (F_CPU/(8 * 8000)) - 1; // (must be <256)
  // turn on CTC mode
  TCCR2B |= (1 << WGM21);
  // Set CS22, CS21 and CS20 bits for 8 prescaler
  TCCR2B |= (0 << CS22) | (1 << CS21) | (0 << CS20);
  // enable timer compare interrupt
  TIMSK2 |= (1 << OCIE2A);
  sei(); // allow interrupts
}

// ADCH
void disableAudioRecording() {
  if (!isAudioRecording) return;
  isAudioRecording = false;

  // Restore back to original 'Arduino' settings
  cli();
  TCCR2A = bit(WGM20);
  TCCR2B = bit(CS22);
  OCR2A = 0;
  OCR2B = 0;
  TIMSK2&=~bit(OCIE2A);                          // Disable interrupt
  sei(); // allow interrupts
}

// Run party mode
void doPartyMode(int speed, bool alwaysMinimum) {
  enableAudioRecording();

  // Dont update unless a new sample has been received.  Very dynamic!
  static unsigned int lastCalc = 0;
  if (lastCalc == lastCalculated) return;
  lastCalc = lastCalculated;

  static bool isAnimationRunning = false;
  static unsigned int target = 0;
  static unsigned int current = 0;

  if (!isAnimationRunning) {
      const unsigned int averageToBeat = (averageValue+4);
      if (loudestValue > averageToBeat) {
        target = (loudestValue - averageToBeat) * 2.5 * speed;
        if (target > 25 * speed) target = 25 * speed;
      } else {
        target = 0;
      }
      isAnimationRunning = true;
  } else {
    if (current < target) current++;
    if (current > target) current--;
    if (current == target) isAnimationRunning = false;
  
    digitalWrite(PIN_GHOST_RED, (current > 5*speed) || ((alwaysMinimum) && (loudestValue>(averageValue+8))));
    digitalWrite(PIN_GHOST_ORANGE, current > 10*speed);
    digitalWrite(PIN_GHOST_BLUE, current > 15*speed);
    digitalWrite(PIN_PACMAN, current > 20*speed); 
  }  
}

// A real vu meter, this one DOES NOT use auto volume gain
void doRealVU() {
    enableAudioRecording();

    // Dont update unless a new sample has been received.  Very dynamic!
    static unsigned int lastCalc = 0;
    if (lastCalc == lastCalculated) return;
    lastCalc = lastCalculated;

    digitalWrite(PIN_GHOST_RED, loudestValue>10);
    digitalWrite(PIN_GHOST_ORANGE, loudestValue>15);
    digitalWrite(PIN_GHOST_BLUE, loudestValue>20);
    digitalWrite(PIN_PACMAN, loudestValue>25); 
}

// Run a fequency mode.  Uses the ArduinoFHT library from http://wiki.openmusiclabs.com/wiki/ArduinoFHT
// You can install the library from the Arduino IDE but this didnt work for me, so I unzipped and copied the FHT folder to C:\Program Files (x86)\Arduino\hardware\arduino\avr\libraries
void runFrequencyMode() {
    enableAudioRecording();

    // Update when a new sample has been received
    static unsigned int lastCalc = 0;
    if (lastCalc == lastCalculated) return;
    lastCalc = lastCalculated;
      
    static unsigned int movingAverage[4] = {0,0,0,0};
    static unsigned int totals[4] = {0,0,0,0};
    static unsigned int fading[4] = {0,0,0,0};

    // Update when 256 samples have been received
    if (newSampleGroup) {
      cli();
      newSampleGroup = false;
      // To compute the FFT first we need the samples in a better format
      for (unsigned int s=0; s<255; s+=2) {
         fht_input[s] = samples[s];
         fht_input[s+1] = 0;
      }
      // Window the data
      fht_window();
      // Re-order the data
      fht_reorder();
      // Run the FHT process
      fht_run();
      // extract the data
      fht_mag_log();
      sei();
     
      // vReal between 0 and 127 contains a strength of frequency at that point.  We're gonna pull out some frequencies
      for (unsigned char a=0; a<32; a++) {
        totals[0] += fht_log_out[a];
        totals[1] += fht_log_out[a+16];
        totals[2] += fht_log_out[a+32];
        totals[3] += fht_log_out[a+48];
      }
  
      // Now maintain a moving average level for these
      for (unsigned char a=0; a<4; a++) {
        totals[a] /=32;  // get average of this group of bucket
        movingAverage[a] = (movingAverage[a] + totals[a]) /2;
        if (totals[a]>(movingAverage[a]+2)) fading[a] = 255;
      }
    }

    // Now light up the relevant light depending on what was detected    
    // The PWM channel for the ghost is in use by the interrupt. So we manually perform a PWM
    ghostPWMValue = fading[0];
    analogWrite(PIN_GHOST_ORANGE, fading[1]);
    analogWrite(PIN_GHOST_BLUE, fading[2]);
    analogWrite(PIN_PACMAN,fading[3]);

    for (unsigned char a=0; a<4; a++)
      if (fading[a]>2) fading[a]-=3;
}

void loop() {  
  
  // Be able to control the current mode via the serial port, starting at mode 0 for off
  if (Serial.available()) {
    unsigned char c = Serial.read();
    if ((c>='0') && (c<='9')) {
      mode = (Modes)(c-'0');
      modeChanged = true;
      EEPROM.put(0,(unsigned char)mode);
    }
  }  

  unsigned long tmp;
  switch (mode) {

    // Mode 0: Off
    case Modes::mOff: 
         turnOffEverything();
         break;

    // Mode 1: Fade in
    case Modes::mOn:
        if (modeChanged) {
          modeChanged = false;
          turnOffEverything();
          for (unsigned char brightness=0; brightness<255; brightness++) {
              analogWrite(PIN_GHOST_RED, brightness);
              analogWrite(PIN_GHOST_ORANGE, brightness);
              analogWrite(PIN_GHOST_BLUE, brightness);
              analogWrite(PIN_PACMAN, brightness); 
              delay(5);
          }
        }
        break;   

    // Mode 2: Colour phasing
    case Modes::mColourPhasing:
         if (modeChanged) {
            modeChanged = false;
            turnOffEverything();
         }         
         tmp = (millis() / 1000) % 3;
         analogWrite(PIN_GHOST_RED, (tmp == 0) ? 255 : 0);
         analogWrite(PIN_GHOST_ORANGE, (tmp == 1) ? 255 : 0);
         analogWrite(PIN_GHOST_BLUE, (tmp == 2) ? 255 : 0);
         analogWrite(PIN_PACMAN, 255); 
         break;

     case Modes::mPartyMode:
          if (modeChanged) {
            modeChanged = false;
            turnOffEverything();
            enableAudioRecording();
          }

          doPartyMode(10, false);
          
          break;

     case Modes::mVUMeter:
          if (modeChanged) {
            modeChanged = false;
            turnOffEverything();
            enableAudioRecording();
          }
          doPartyMode(4, true);          
          break;          

    case Modes::mRealVUMeter:
          if (modeChanged) {
            modeChanged = false;
            turnOffEverything();
            enableAudioRecording();
          }
          doRealVU();          
          break;      

    case Modes::mFrequencyMode:
          if (modeChanged) {
            modeChanged = false;
            turnOffEverything();
            enableAudioRecording();
          }
          runFrequencyMode();          
          break;      
  }
}

Credits

RobSmithDev

RobSmithDev

11 projects • 21 followers
I'm a Youtuber, programmer, electronics hobbyist and Amiga/Retro Fan. Creator of DrawBridge FloppyBridge DiskFlashback Retro.Directory

Comments