| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464 |
- #!/usr/bin/perl
- use strict;
- use warnings;
- use POSIX;
- use File::Touch;
- use File::Basename;
- use Time::HiRes qw( usleep sleep time );
- use IPC::System::Simple qw( system capture );
- use Sys::Hostname;
- # Local modules
- use Cwd qw(abs_path);
- use File::Basename qw(dirname);
- use lib dirname(abs_path($0));
- use DMX;
- use Audio;
- # Effect Prototypes
- sub red_alert($$$);
- sub lsr_init($$$);
- sub lsr_run($$$);
- sub lsr_loop($$$);
- # User config
- my $MEDIA_PATH = `~/bin/video/mediaPath` . '/DMX';
- my @CHANNELS = (1, 2, 4, 5, 6, 7, 8, 9, 13, 14, 15);
- my %EFFECTS = ();
- my %FILES = ();
- # Red Alert
- $FILES{'RED_ALERT'} = 'DMX/Red Alert.mp3';
- $EFFECTS{'RED_ALERT'} = { 'cmd' => \&red_alert };
- # Light-switch rave
- $EFFECTS{'LSR'} = { 'cmd' => \&lsr_init, 'next' => \&lsr_run, 'loop' => \&lsr_loop, 'done' => \&lsr_done };
- $FILES{'LSR'} = 'DMX/Rave.mp3';
- # Utility prototypes
- sub loadFiles($);
- sub ampWait($$$);
- # App config
- my $DATA_DIR = DMX::dataDir();
- my $STATE_SOCK = 'RAVE_CMD';
- my $OUTPUT_FILE = $DATA_DIR . $STATE_SOCK;
- my $RAVE_FILE = $DATA_DIR . 'RAVE';
- my $EFFECT_FILE = $DATA_DIR . 'EFFECT';
- my $PUSH_TIMEOUT = 20;
- my $PULL_TIMEOUT = $PUSH_TIMEOUT * 3;
- my $DELAY = $PULL_TIMEOUT / 2;
- my $AMP_DELAY = 6;
- my $AMP_BOOTING = 0;
- my $AMP_START = undef();
- my $AMP_TIMEOUT = $PUSH_TIMEOUT / 2;
- my $CLEANUP_TIME = undef();
- # Debug
- my $DEBUG = 0;
- if ($ENV{'DEBUG'}) {
- $DEBUG = 1;
- }
- # Load all defined audio files (which will kill us if they are not availble)
- loadFiles(\%FILES);
- # Sockets
- DMX::stateSocket($STATE_SOCK);
- DMX::stateSubscribe($STATE_SOCK);
- # State
- my %exists = ();
- my %last = ();
- my $pullLast = time();
- my $update = 0;
- my $PID = undef();
- my $NAME = undef();
- my $NEXT = undef();
- my $PID_DATA = undef();
- # Always clear the RAVE and EFFECT files
- if (-e $RAVE_FILE) {
- unlink($RAVE_FILE);
- }
- if (-e $EFFECT_FILE) {
- unlink($EFFECT_FILE);
- }
- # Loop forever
- while (1) {
- # State is transient during a RAVE
- my $newState = 'OFF';
- %last = %exists;
- # Reduce the select delay when we're processing background effects
- # We don't want to hang waiting for event updates while we're running the show
- my $delay = $DELAY;
- if (defined($PID)) {
- $delay = 0.001;
- } elsif (defined($NEXT)) {
- $delay = 0.25;
- }
- # Wait for state updates
- {
- my $cmdState = DMX::readState($delay, \%exists, undef(), undef());
- if (defined($cmdState)) {
- $newState = $cmdState;
- $pullLast = time();
- }
- }
- # Die if we don't see regular updates
- if (time() - $pullLast > $PULL_TIMEOUT) {
- die('No update on state socket in past ' . $PULL_TIMEOUT . " seconds. Exiting...\n");
- }
- # Handle "STOP" commands
- if ($newState eq 'STOP') {
- if (defined($PID)) {
- if ($DEBUG) {
- print STDERR "Ending background processing\n";
- }
- Audio::stopAll();
- kill(SIGTERM, $PID);
- }
- $update = 1;
- }
- # Reap zombie children
- if (defined($PID) && waitpid($PID, WNOHANG) > 0) {
- if ($DEBUG) {
- print STDERR 'Reaped child: ' . $PID . "\n";
- }
- $CLEANUP_TIME = Time::HiRes::time();
- }
- # Cleanup on timer or child exit
- if (defined($CLEANUP_TIME) && $CLEANUP_TIME <= Time::HiRes::time()) {
- # Run the cleanup routine (if any)
- if ($EFFECTS{$NAME}{'done'}) {
- $EFFECTS{$NAME}{'done'}($NAME, \%exists, $EFFECTS{$NAME});
- }
- # Forget our local bypass state
- $PID = undef();
- $CLEANUP_TIME = undef();
- $NAME = undef();
- $NEXT = undef();
- $PID_DATA = undef();
- # Clear the RAVE and EFFECT flags
- if (-e $RAVE_FILE) {
- unlink($RAVE_FILE);
- }
- if (-e $EFFECT_FILE) {
- unlink($EFFECT_FILE);
- }
- }
- # Continue special processing
- if (defined($NEXT)) {
- $NEXT->($NAME, \%exists, $EFFECTS{$NAME});
- next;
- }
- # Alert continuously on ALARM
- if ($exists{'ALARM'}) {
- $newState = 'RED_ALERT';
- }
- # Handle effects by name
- if (defined($EFFECTS{$newState})) {
- # Initiate the EFFECT state
- touch($EFFECT_FILE);
- # Store the effect name
- $NAME = $newState;
- # Dispatch the effect
- $EFFECTS{$NAME}{'cmd'}($NAME, \%exists, $EFFECTS{$NAME});
- # Clear the EFFECT state if there is no background process
- if (!defined($PID)) {
- if (-e $EFFECT_FILE) {
- unlink($EFFECT_FILE);
- }
- }
- next;
- }
- # Clear the RAVE and EFFECT flags, just in case
- if (-e $RAVE_FILE) {
- unlink($RAVE_FILE);
- die("Unexpected RAVE file\n");
- }
- if (-e $EFFECT_FILE) {
- unlink($EFFECT_FILE);
- die("Unexpected EFFECT file\n");
- }
- # Ensure our audio files are always loaded
- loadFiles(\%FILES);
- }
- # ======================================
- # Utility routines
- # ======================================
- sub loadFiles($) {
- my ($files) = @_;
- foreach my $file (keys(%{$files})) {
- if (!Audio::loaded($file)) {
- Audio::load($file, $files->{$file});
- }
- }
- }
- sub ampWait($$$) {
- my ($name, $exists, $params) = @_;
- if ($DEBUG) {
- print STDERR "ampWait()\n";
- }
- # Ensure we don't loop forever
- if (!defined($AMP_START)) {
- $AMP_START = Time::HiRes::time();
- } elsif (Time::HiRes::time() - $AMP_START > $AMP_TIMEOUT) {
- die("Timeout waiting for amp to start\n");
- }
- # Just loop until the amp is up, then set a new "next" handler
- if ($exists{'AMPLIFIER'}) {
- if ($DEBUG) {
- print STDERR "\tAmplifier ready\n";
- }
- # Once the amp is up, be sure we've started "RAVE" mode for audio output
- if ($exists{'AUDIO_STATE'} eq 'RAVE') {
- if ($DEBUG) {
- print STDERR "\tAudio device ready\n";
- }
- # Select the next function
- if ($params->{'next'}) {
- $NEXT = $params->{'next'};
- } else {
- $NEXT = undef();
- }
- # Always wait just a bit for good luck
- sleep($AMP_DELAY / 4);
- }
- # Wait for the amp to boot if it wasn't running when we first checked
- if ($AMP_BOOTING) {
- if ($DEBUG) {
- print STDERR "\tWaiting " . $AMP_DELAY . " seconds for amp to boot\n";
- }
- sleep($AMP_DELAY);
- }
- # Reset the amp boot delay and amp start time now that it's running
- $AMP_BOOTING = 0;
- undef($AMP_START);
- } else {
- # The amp wasn't up when we checked, so it will need a boot delay
- $AMP_BOOTING = 1;
- }
- # Do not pass GO, do not collect $200
- return 1;
- }
- # ======================================
- # Effects routines
- # ======================================
- sub red_alert($$$) {
- my ($name, $exists, $params) = @_;
- if ($DEBUG) {
- print STDERR "red_alert()\n";
- }
- my $ramp = 450;
- my $sleep = $ramp;
- # Bring the B & G channels down to 0
- my @other = ();
- push(@other, { 'channel' => 13, 'value' => 0, 'time' => 0 });
- push(@other, { 'channel' => 15, 'value' => 0, 'time' => 0 });
- foreach my $data (@other) {
- DMX::dim($data);
- }
- # Set the high/low values for the ramp
- my %high = ('channel' => 14, 'value' => 255, 'time' => $ramp);
- my %low = %high;
- $low{'value'} = 64;
- # Three blasts
- for (my $i = 0 ; $i < 3 ; $i++) {
- DMX::dim(\%high);
- Audio::play($name);
- DMX::dim(\%low);
- usleep($sleep * 1000);
- }
- # Follow through on the loop
- return 0;
- }
- sub lsr_init($$$) {
- my ($name, $exists, $params) = @_;
- if ($DEBUG) {
- print STDERR "lsr_init()\n";
- }
- # Initiate the RAVE state
- DMX::say('Light switch rave: initiated');
- touch($RAVE_FILE);
- # Save data for future runs
- my %data = ();
- $PID_DATA = \%data;
- # Initialize the channels hash
- my %chans = ();
- foreach my $chan (@CHANNELS) {
- $chans{$chan} = 0;
- }
- $data{'channels'} = \%chans;
- $data{'num_channels'} = scalar(keys(%chans));
- $data{'live_channels'} = 0;
- # Dim while we wait
- my @data_set = ();
- foreach my $chan (@CHANNELS) {
- push(@data_set, { 'channel' => $chan, 'value' => 0, 'time' => $AMP_DELAY * 1000 });
- }
- DMX::applyDataset(\@data_set, 'RAVE', $OUTPUT_FILE);
- # Wait for the amp to power up
- $NEXT = \&Wait;
- # Do not pass GO, do not collect $200
- return 1;
- }
- sub lsr_run($$$) {
- my ($name, $exists, $params) = @_;
- if ($DEBUG) {
- print STDERR "lsr_run()\n";
- }
- # Wait just a second for annoucements
- sleep(1);
- # Play a short burst of silence to get all the audio devices in-sync
- Audio::play('SILENCE');
- # Play the sound in the background
- Audio::background($name);
- # Set a cleanup timer
- # Now + playback + fade + buffer
- $CLEANUP_TIME = Time::HiRes::time() + 45.60 + 2 + 1;
- # Record our start time
- $PID_DATA->{'start'} = Time::HiRes::time();
- # Start the main loop
- $NEXT = $params->{'loop'};
- # Do not pass GO, do not collect $200
- return 1;
- }
- sub lsr_loop($$$) {
- my ($name, $exists, $params) = @_;
- if ($DEBUG) {
- print STDERR "lsr_loop()\n";
- }
- # Config
- my $max_dur = 375;
- my $max_val = 255;
- my $reserve = 0.75;
- my $ramp_dur = 10.40;
- my $hit_pos = 43.35;
- my $hit_dur = 100;
- my $fade_pos = 43.85;
- my $fade_dur = 2000;
- # How long have we been playing
- my $elapsed = Time::HiRes::time() - $PID_DATA->{'start'};
- if ($DEBUG) {
- print STDERR 'Elapsed: ' . $elapsed . "\n";
- }
- # Ramp up the number of channels in our effect
- if ($elapsed < $ramp_dur) {
- my $ratio = $PID_DATA->{'live_channels'} / ($PID_DATA->{'num_channels'} * (1 - $reserve));
- if ($PID_DATA->{'live_channels'} < 1 || $ratio < $elapsed / $ramp_dur) {
- my @keys = keys(%{ $PID_DATA->{'channels'} });
- my $index = int(rand($PID_DATA->{'num_channels'}));
- while ($PID_DATA->{'channels'}->{ $keys[$index] } > 0) {
- $index = int(rand($PID_DATA->{'num_channels'}));
- }
- $PID_DATA->{'channels'}->{ $keys[$index] } = 1;
- $PID_DATA->{'live_channels'}++;
- }
- } else {
- if ($PID_DATA->{'live_channels'} < $PID_DATA->{'num_channels'}) {
- foreach my $chan (keys(%{ $PID_DATA->{'channels'} })) {
- $PID_DATA->{'channels'}->{$chan} = 1;
- }
- $PID_DATA->{'live_channels'} = $PID_DATA->{'num_channels'};
- }
- }
- # Prepare data for each channel
- my @data_set = ();
- my $wait = $max_dur;
- foreach my $chan (keys(%{ $PID_DATA->{'channels'} })) {
- my $val = 0;
- my $dur = 0;
- # Random data on enabled channels until the fade
- if ($elapsed < $hit_pos) {
- if ($PID_DATA->{'channels'}->{$chan} > 0) {
- $dur = int(rand($max_dur));
- $val = int(rand($max_val));
- }
- } elsif ($elapsed < $fade_pos) {
- $val = $max_val;
- $dur = $hit_dur;
- $wait = ($fade_pos - $hit_pos) * 1000;
- } else {
- $val = 0;
- $dur = $fade_dur;
- $wait = $dur;
- }
- push(@data_set, { 'channel' => $chan, 'value' => $val, 'time' => $dur });
- }
- # Push the data set
- DMX::applyDataset(\@data_set, 'RAVE', $OUTPUT_FILE);
- # Wait for the fade interval
- usleep($wait * 1000);
- }
- sub lsr_done($$$) {
- my ($name, $exists, $params) = @_;
- if ($DEBUG) {
- print STDERR "lsr_done()\n";
- }
- DMX::say('Light switch rave: terminated');
- }
|