Hardware components | ||||||
| × | 1 | ||||
| × | 1 | ||||
Software apps and online services | ||||||
|
So I've had this Pacman light for some time, but I don't really like the functionality it has. The main one being the sound to light effect requires the volume to be far too loud before it reacts,
In this video I show how the controller inside can be replaced with an Arduino, and then we re-program it to contain all the original effects and many more. The code shows how to make a sound-to-light product that can even react to different sound frequencies.
// 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;
}
}
RobSmithDev
11 projects • 21 followers
I'm a Youtuber, programmer, electronics hobbyist and Amiga/Retro Fan. Creator of DrawBridge FloppyBridge DiskFlashback Retro.Directory
Comments