rave.pl 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. #!/usr/bin/perl
  2. use strict;
  3. use warnings;
  4. use POSIX;
  5. use File::Touch;
  6. use Time::HiRes qw( usleep );
  7. # Local modules
  8. use Cwd qw(abs_path);
  9. use File::Basename qw(dirname);
  10. use lib dirname(abs_path($0));
  11. use DMX;
  12. # Prototypes
  13. sub red_alert($$);
  14. sub rave_init($$);
  15. sub lsr_loop();
  16. # User config
  17. my @CHANNELS = (1, 2, 4, 5, 6, 7, 8, 9, 13, 14, 15);
  18. my %EFFECTS = (
  19. 'RED_ALERT' => { 'cmd' => \&red_alert },
  20. 'LSR' => { 'cmd' => \&rave_init, 'file' => '/mnt/media/DMX/Rave.mp3', 'next' => \&lsr_loop },
  21. );
  22. # App config
  23. my $DATA_DIR = DMX::dataDir();
  24. my $OUTPUT_FILE = $DATA_DIR . 'RAVE_CMD';
  25. my $STATE_SOCK = $OUTPUT_FILE . '.socket';
  26. my $RAVE_FILE = $DATA_DIR . 'RAVE';
  27. my $EFFECT_FILE = $DATA_DIR . 'EFFECT';
  28. my $PUSH_TIMEOUT = 20;
  29. my $PULL_TIMEOUT = $PUSH_TIMEOUT * 3;
  30. my $DELAY = $PULL_TIMEOUT / 2;
  31. # Debug
  32. my $DEBUG = 0;
  33. if ($ENV{'DEBUG'}) {
  34. $DEBUG = 1;
  35. }
  36. # Sockets
  37. DMX::stateSocket($STATE_SOCK);
  38. DMX::stateSubscribe($STATE_SOCK);
  39. # State
  40. my %exists = ();
  41. my $pullLast = time();
  42. my $update = 0;
  43. my $PID = undef();
  44. my $EFFECT = undef();
  45. my $PID_DATA = undef();
  46. # Always clear the RAVE and EFFECT files
  47. if (-e $RAVE_FILE) {
  48. unlink($RAVE_FILE);
  49. }
  50. if (-e $EFFECT_FILE) {
  51. unlink($EFFECT_FILE);
  52. }
  53. # Loop forever
  54. while (1) {
  55. # State is transient during a RAVE
  56. my $newState = 'OFF';
  57. # Reduce the select delay when we're processing background effects
  58. # We don't want to hang waiting for event updates while we're running the show
  59. my $delay = $DELAY;
  60. if (defined($PID)) {
  61. $delay = 0.001;
  62. }
  63. # Wait for state updates
  64. {
  65. my %existsTmp = ();
  66. my $cmdState = DMX::readState($delay, \%existsTmp, undef(), undef());
  67. if (defined($cmdState)) {
  68. $newState = $cmdState;
  69. $pullLast = time();
  70. }
  71. # Only record valid exists hashes
  72. if (scalar(keys(%existsTmp)) < 1) {
  73. %exists = %existsTmp;
  74. }
  75. }
  76. # Die if we don't see regular updates
  77. if (time() - $pullLast > $PULL_TIMEOUT) {
  78. die('No update on state socket in past ' . $PULL_TIMEOUT . " seconds. Exiting...\n");
  79. }
  80. # Handle "STOP" commands
  81. if ($newState eq 'STOP') {
  82. if (defined($PID)) {
  83. if ($DEBUG) {
  84. print STDERR "Ending background processing\n";
  85. }
  86. kill(SIGTERM, $PID);
  87. }
  88. $update = 1;
  89. }
  90. # Reap zombie children
  91. if (defined($PID)) {
  92. my $kid = waitpid($PID, WNOHANG);
  93. if ($kid > 0) {
  94. if ($DEBUG) {
  95. print STDERR 'Reaped child: ' . $PID . "\n";
  96. }
  97. # Forget our local bypass state
  98. $PID = undef();
  99. $EFFECT = undef();
  100. $PID_DATA = undef();
  101. # Clear the RAVE and EFFECT flags
  102. if (-e $RAVE_FILE) {
  103. unlink($RAVE_FILE);
  104. }
  105. if (-e $EFFECT_FILE) {
  106. unlink($EFFECT_FILE);
  107. }
  108. }
  109. }
  110. # Continue special processing
  111. if (defined($EFFECT)) {
  112. $EFFECT->();
  113. next;
  114. }
  115. # Handle effects by name
  116. if (defined($EFFECTS{$newState})) {
  117. # Initiate the EFFECT state
  118. touch($EFFECT_FILE);
  119. # Dispatch the effect
  120. $EFFECTS{$newState}{'cmd'}(\%exists, $EFFECTS{$newState});
  121. # Clear the EFFECT state if there is no background process
  122. if (!defined($PID)) {
  123. if (-e $EFFECT_FILE) {
  124. unlink($EFFECT_FILE);
  125. }
  126. }
  127. next;
  128. }
  129. # Clear the RAVE and EFFECT flags, just in case
  130. if (-e $RAVE_FILE) {
  131. unlink($RAVE_FILE);
  132. die("Unexpected RAVE file\n");
  133. }
  134. if (-e $EFFECT_FILE) {
  135. unlink($EFFECT_FILE);
  136. die("Unexpected EFFECT file\n");
  137. }
  138. }
  139. # ======================================
  140. # Effects routines
  141. # ======================================
  142. sub red_alert($$) {
  143. my ($exists, $effect) = @_;
  144. if ($DEBUG) {
  145. print STDERR "red_alert()\n";
  146. }
  147. my $file = '/mnt/media/DMX/Red Alert.mp3';
  148. my @sound = ('afplay', $file);
  149. my $ramp = 450;
  150. my $sleep = $ramp;
  151. # Stat the file to bring the network up-to-date
  152. stat($file);
  153. # Bring the B & G channels down to 0
  154. my @other = ();
  155. push(@other, { 'channel' => 13, 'value' => 0, 'time' => 0 });
  156. push(@other, { 'channel' => 15, 'value' => 0, 'time' => 0 });
  157. foreach my $data (@other) {
  158. DMX::dim($data);
  159. }
  160. my %high = ('channel' => 14, 'value' => 255, 'time' => $ramp);
  161. my %low = %high;
  162. $low{'value'} = 64;
  163. DMX::dim(\%high);
  164. system(@sound);
  165. DMX::dim(\%low);
  166. usleep($sleep * 1000);
  167. DMX::dim(\%high);
  168. system(@sound);
  169. DMX::dim(\%low);
  170. usleep($sleep * 1000);
  171. DMX::dim(\%high);
  172. system(@sound);
  173. DMX::dim(\%low);
  174. usleep($sleep * 1000);
  175. # Follow through on the loop
  176. return 0;
  177. }
  178. sub rave_init($$) {
  179. my ($exists, $effect) = @_;
  180. if ($DEBUG) {
  181. print STDERR "rave_init()\n";
  182. }
  183. if (!defined($effect->{'file'}) || !-r $effect->{'file'}) {
  184. die('Invalid RAVE audio file: ' . $effect->{'file'} . "\n");
  185. }
  186. # Config
  187. my $SND_APP = 'afplay';
  188. my $SILENCE = '/mnt/media/DMX/Silence.wav';
  189. my $SIL_DELAY = 1.3;
  190. my $AMP_SHORT = 5;
  191. my $AMP_LONG = $AMP_SHORT + 5;
  192. # Stat the file to bring the network up-to-date
  193. stat($SILENCE);
  194. stat($effect->{'file'});
  195. # Initiate the RAVE state
  196. touch($RAVE_FILE);
  197. # Setup our loop handler
  198. if ($effect->{'next'}) {
  199. $EFFECT = $effect->{'next'};
  200. }
  201. # Save data for future runs
  202. my %data = ();
  203. $PID_DATA = \%data;
  204. # Initialize the channels hash
  205. my %chans = ();
  206. foreach my $chan (@CHANNELS) {
  207. $chans{$chan} = 0;
  208. }
  209. $data{'channels'} = \%chans;
  210. $data{'num_channels'} = scalar(keys(%chans));
  211. $data{'live_channels'} = 0;
  212. # Reduce the amp delay if the amp is already on
  213. my $amp_wait = $AMP_LONG;
  214. if ($exists->{'AMPLIFIER'}) {
  215. $amp_wait = $AMP_SHORT;
  216. }
  217. # Dim while we wait
  218. my @data_set = ();
  219. foreach my $chan (@CHANNELS) {
  220. push(@data_set, { 'channel' => $chan, 'value' => 0, 'time' => $amp_wait / 2 * 1000 });
  221. }
  222. DMX::applyDataset(\@data_set, 'RAVE', $OUTPUT_FILE);
  223. # Wait for the amp to power up
  224. if ($DEBUG) {
  225. print STDERR 'Waiting ' . $amp_wait . " seconds for the amp to boot\n";
  226. }
  227. sleep($amp_wait - $SIL_DELAY);
  228. # Play a short burst of silence to get all the audio in-sync
  229. my @sound = ($SND_APP, $SILENCE);
  230. system(@sound);
  231. # Play the sound in a child (i.e. in the background)
  232. $PID = fork();
  233. if (defined($PID) && $PID == 0) {
  234. my @sound = ($SND_APP, $effect->{'file'});
  235. exec(@sound)
  236. or die('Unable to play sound: ' . join(' ', @sound) . "\n");
  237. }
  238. # Record our start time
  239. $data{'start'} = Time::HiRes::time();
  240. # Do not pass GO, do not collect $200
  241. return 1;
  242. }
  243. sub lsr_loop() {
  244. if ($DEBUG) {
  245. print STDERR "lsr_loop()\n";
  246. }
  247. # Config
  248. my $max_dur = 375;
  249. my $max_val = 255;
  250. my $reserve = 0.75;
  251. my $ramp_dur = 10.20;
  252. my $hit_pos = 43.15;
  253. my $hit_dur = 100;
  254. my $fade_pos = 43.65;
  255. my $fade_dur = 2000;
  256. # How long have we been playing
  257. my $elapsed = Time::HiRes::time() - $PID_DATA->{'start'};
  258. if ($DEBUG) {
  259. print STDERR 'Elapsed: ' . $elapsed . "\n";
  260. }
  261. # Ramp up the number of channels in our effect
  262. if ($elapsed < $ramp_dur) {
  263. my $ratio = $PID_DATA->{'live_channels'} / ($PID_DATA->{'num_channels'} * (1 - $reserve));
  264. if ($PID_DATA->{'live_channels'} < 1 || $ratio < $elapsed / $ramp_dur) {
  265. my @keys = keys(%{ $PID_DATA->{'channels'} });
  266. my $index = int(rand($PID_DATA->{'num_channels'}));
  267. while ($PID_DATA->{'channels'}->{ $keys[$index] } > 0) {
  268. $index = int(rand($PID_DATA->{'num_channels'}));
  269. }
  270. $PID_DATA->{'channels'}->{ $keys[$index] } = 1;
  271. $PID_DATA->{'live_channels'}++;
  272. }
  273. } else {
  274. if ($PID_DATA->{'live_channels'} < $PID_DATA->{'num_channels'}) {
  275. foreach my $chan (keys(%{ $PID_DATA->{'channels'} })) {
  276. $PID_DATA->{'channels'}->{$chan} = 1;
  277. }
  278. $PID_DATA->{'live_channels'} = $PID_DATA->{'num_channels'};
  279. }
  280. }
  281. # Prepare data for each channel
  282. my @data_set = ();
  283. my $wait = $max_dur;
  284. foreach my $chan (keys(%{ $PID_DATA->{'channels'} })) {
  285. my $val = 0;
  286. my $dur = 0;
  287. # Random data on enabled channels until the fade
  288. if ($elapsed < $hit_pos) {
  289. if ($PID_DATA->{'channels'}->{$chan} > 0) {
  290. $dur = int(rand($max_dur));
  291. $val = int(rand($max_val));
  292. }
  293. } elsif ($elapsed < $fade_pos) {
  294. $val = $max_val;
  295. $dur = $hit_dur;
  296. $wait = ($fade_pos - $hit_pos) * 1000;
  297. } else {
  298. $val = 0;
  299. $dur = $fade_dur;
  300. $wait = $dur;
  301. }
  302. push(@data_set, { 'channel' => $chan, 'value' => $val, 'time' => $dur });
  303. }
  304. # Push the data set
  305. DMX::applyDataset(\@data_set, 'RAVE', $OUTPUT_FILE);
  306. # Wait for the fade interval
  307. usleep($wait * 1000);
  308. }