rave.pl 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. #!/usr/bin/perl
  2. use strict;
  3. use warnings;
  4. use POSIX;
  5. use File::Touch;
  6. use File::Basename;
  7. use Time::HiRes qw( usleep sleep time );
  8. use IPC::System::Simple qw( system capture );
  9. # Local modules
  10. use Cwd qw(abs_path);
  11. use File::Basename qw(dirname);
  12. use lib dirname(abs_path($0));
  13. use DMX;
  14. use Audio;
  15. # Effect Prototypes
  16. sub red_alert($$$);
  17. sub lsr_init($$$);
  18. sub lsr_run($$$);
  19. sub lsr_loop($$$);
  20. # User config
  21. my $MEDIA_PATH = `~/bin/video/mediaPath` . '/DMX';
  22. my @CHANNELS = (1, 2, 4, 5, 6, 7, 8, 9, 13, 14, 15);
  23. my %EFFECTS = (
  24. 'RED_ALERT' => { 'cmd' => \&red_alert },
  25. 'LSR' => { 'cmd' => \&lsr_init, 'next' => \&lsr_run, 'loop' => \&lsr_loop },
  26. );
  27. my %FILES = (
  28. 'RED_ALERT' => 'DMX/Red Alert.mp3',
  29. 'LSR' => 'DMX/Rave.mp3',
  30. );
  31. # Utility prototypes
  32. sub ampWait($$$);
  33. # App config
  34. my $DATA_DIR = DMX::dataDir();
  35. my $OUTPUT_FILE = $DATA_DIR . 'RAVE_CMD';
  36. my $STATE_SOCK = $OUTPUT_FILE . '.socket';
  37. my $RAVE_FILE = $DATA_DIR . 'RAVE';
  38. my $EFFECT_FILE = $DATA_DIR . 'EFFECT';
  39. my $PUSH_TIMEOUT = 20;
  40. my $PULL_TIMEOUT = $PUSH_TIMEOUT * 3;
  41. my $DELAY = $PULL_TIMEOUT / 2;
  42. my $AMP_DELAY = 7;
  43. my $AMP_BOOTING = 0;
  44. # Debug
  45. my $DEBUG = 0;
  46. if ($ENV{'DEBUG'}) {
  47. $DEBUG = 1;
  48. }
  49. # Load all our audio files
  50. Audio::init();
  51. foreach my $file (keys(%FILES)) {
  52. Audio::add($file, $FILES{$file});
  53. Audio::load($file);
  54. }
  55. # Sockets
  56. DMX::stateSocket($STATE_SOCK);
  57. DMX::stateSubscribe($STATE_SOCK);
  58. # State
  59. my %exists = ();
  60. my $pullLast = time();
  61. my $update = 0;
  62. my $PID = undef();
  63. my $NAME = undef();
  64. my $NEXT = undef();
  65. my $PID_DATA = undef();
  66. # Always clear the RAVE and EFFECT files
  67. if (-e $RAVE_FILE) {
  68. unlink($RAVE_FILE);
  69. }
  70. if (-e $EFFECT_FILE) {
  71. unlink($EFFECT_FILE);
  72. }
  73. # Loop forever
  74. while (1) {
  75. # State is transient during a RAVE
  76. my $newState = 'OFF';
  77. # Reduce the select delay when we're processing background effects
  78. # We don't want to hang waiting for event updates while we're running the show
  79. my $delay = $DELAY;
  80. if (defined($PID)) {
  81. $delay = 0.001;
  82. } elsif (defined($NEXT)) {
  83. $delay = 0.25;
  84. }
  85. # Wait for state updates
  86. {
  87. my %existsTmp = ();
  88. my $cmdState = DMX::readState($delay, \%existsTmp, undef(), undef());
  89. if (defined($cmdState)) {
  90. $newState = $cmdState;
  91. $pullLast = time();
  92. }
  93. # Only record valid exists hashes
  94. if (scalar(keys(%existsTmp)) > 0) {
  95. %exists = %existsTmp;
  96. }
  97. }
  98. # Die if we don't see regular updates
  99. if (time() - $pullLast > $PULL_TIMEOUT) {
  100. die('No update on state socket in past ' . $PULL_TIMEOUT . " seconds. Exiting...\n");
  101. }
  102. # Handle "STOP" commands
  103. if ($newState eq 'STOP') {
  104. if (defined($PID)) {
  105. if ($DEBUG) {
  106. print STDERR "Ending background processing\n";
  107. }
  108. Audio::stop(undef());
  109. kill(SIGTERM, $PID);
  110. }
  111. $update = 1;
  112. }
  113. # Reap zombie children
  114. if (defined($PID)) {
  115. my $kid = waitpid($PID, WNOHANG);
  116. if ($kid > 0) {
  117. if ($DEBUG) {
  118. print STDERR 'Reaped child: ' . $PID . "\n";
  119. }
  120. # Forget our local bypass state
  121. $PID = undef();
  122. $NAME = undef();
  123. $NEXT = undef();
  124. $PID_DATA = undef();
  125. # Clear the RAVE and EFFECT flags
  126. if (-e $RAVE_FILE) {
  127. unlink($RAVE_FILE);
  128. }
  129. if (-e $EFFECT_FILE) {
  130. unlink($EFFECT_FILE);
  131. }
  132. }
  133. }
  134. # Continue special processing
  135. if (defined($NEXT)) {
  136. $NEXT->($NAME, \%exists, $EFFECTS{$NAME});
  137. next;
  138. }
  139. # Handle effects by name
  140. if (defined($EFFECTS{$newState})) {
  141. # Initiate the EFFECT state
  142. touch($EFFECT_FILE);
  143. # Store the effect name
  144. $NAME = $newState;
  145. # Dispatch the effect
  146. $EFFECTS{$NAME}{'cmd'}($NAME, \%exists, $EFFECTS{$NAME});
  147. # Clear the EFFECT state if there is no background process
  148. if (!defined($PID)) {
  149. if (-e $EFFECT_FILE) {
  150. unlink($EFFECT_FILE);
  151. }
  152. }
  153. next;
  154. }
  155. # Clear the RAVE and EFFECT flags, just in case
  156. if (-e $RAVE_FILE) {
  157. unlink($RAVE_FILE);
  158. die("Unexpected RAVE file\n");
  159. }
  160. if (-e $EFFECT_FILE) {
  161. unlink($EFFECT_FILE);
  162. die("Unexpected EFFECT file\n");
  163. }
  164. }
  165. # ======================================
  166. # Utility routines
  167. # ======================================
  168. sub runApplescript($) {
  169. my ($script) = @_;
  170. if ($DEBUG) {
  171. print STDERR 'Running AppleScript: ' . $script . "\n";
  172. }
  173. my $retval = capture('osascript', '-e', $script);
  174. if ($DEBUG) {
  175. print STDERR "\tAppleScript result: " . $retval . "\n";
  176. }
  177. return $retval;
  178. }
  179. sub ampWait($$$) {
  180. my ($name, $exists, $params) = @_;
  181. if ($DEBUG) {
  182. print STDERR "ampWait()\n";
  183. }
  184. # Just loop until the amp is up, then set a new "next" handler
  185. if ($exists{'AMPLIFIER'}) {
  186. if ($DEBUG) {
  187. print STDERR "\tAmplifier ready\n";
  188. }
  189. # Once the amp is up, be sure we've started "RAVE" mode for audio output
  190. if ($exists{'AUDIO_STATE'} eq 'RAVE') {
  191. if ($DEBUG) {
  192. print STDERR "\tAudio device ready\n";
  193. }
  194. if ($params->{'next'}) {
  195. $NEXT = $params->{'next'};
  196. } else {
  197. $NEXT = undef();
  198. }
  199. }
  200. # Wait for the amp to boot if it wasn't running when we first checked
  201. if ($AMP_BOOTING) {
  202. if ($DEBUG) {
  203. print STDERR "\tWaiting " . $AMP_DELAY . "seconds for amp to boot\n";
  204. }
  205. sleep($AMP_DELAY);
  206. }
  207. # Reset the amp boot delay, now that it's running
  208. $AMP_BOOTING = 0;
  209. } else {
  210. # The amp wasn't up when we checked, so it will need a boot delay
  211. $AMP_BOOTING = 1;
  212. }
  213. # Do not pass GO, do not collect $200
  214. return 1;
  215. }
  216. # ======================================
  217. # Effects routines
  218. # ======================================
  219. sub red_alert($$$) {
  220. my ($name, $exists, $params) = @_;
  221. if ($DEBUG) {
  222. print STDERR "red_alert()\n";
  223. }
  224. my $ramp = 450;
  225. my $sleep = $ramp;
  226. # Bring the B & G channels down to 0
  227. my @other = ();
  228. push(@other, { 'channel' => 13, 'value' => 0, 'time' => 0 });
  229. push(@other, { 'channel' => 15, 'value' => 0, 'time' => 0 });
  230. foreach my $data (@other) {
  231. DMX::dim($data);
  232. }
  233. # Set the high/low values for the ramp
  234. my %high = ('channel' => 14, 'value' => 255, 'time' => $ramp);
  235. my %low = %high;
  236. $low{'value'} = 64;
  237. # Three blasts
  238. for (my $i = 0 ; $i < 3 ; $i++) {
  239. DMX::dim(\%high);
  240. Audio::play($name);
  241. DMX::dim(\%low);
  242. usleep($sleep * 1000);
  243. }
  244. # Follow through on the loop
  245. return 0;
  246. }
  247. sub lsr_init($$$) {
  248. my ($name, $exists, $params) = @_;
  249. if ($DEBUG) {
  250. print STDERR "lsr_init()\n";
  251. }
  252. # Initiate the RAVE state
  253. touch($RAVE_FILE);
  254. # Save data for future runs
  255. my %data = ();
  256. $PID_DATA = \%data;
  257. # Initialize the channels hash
  258. my %chans = ();
  259. foreach my $chan (@CHANNELS) {
  260. $chans{$chan} = 0;
  261. }
  262. $data{'channels'} = \%chans;
  263. $data{'num_channels'} = scalar(keys(%chans));
  264. $data{'live_channels'} = 0;
  265. # Dim while we wait
  266. my @data_set = ();
  267. foreach my $chan (@CHANNELS) {
  268. push(@data_set, { 'channel' => $chan, 'value' => 0, 'time' => $AMP_DELAY * 1000 });
  269. }
  270. DMX::applyDataset(\@data_set, 'RAVE', $OUTPUT_FILE);
  271. # Wait for the amp to power up
  272. $NEXT = \&ampWait;
  273. # Do not pass GO, do not collect $200
  274. return 1;
  275. }
  276. sub lsr_run($$$) {
  277. my ($name, $exists, $params) = @_;
  278. if ($DEBUG) {
  279. print STDERR "lsr_run()\n";
  280. }
  281. # Play a short burst of silence to get all the audio devices in-sync
  282. Audio::play('SILENCE');
  283. # Play the sound in a child (i.e. in the background)
  284. $PID = fork();
  285. if (defined($PID) && $PID == 0) {
  286. Audio::play($name);
  287. exit(0);
  288. }
  289. # Record our start time
  290. $PID_DATA->{'start'} = Time::HiRes::time();
  291. # Start the main loop
  292. $NEXT = $params->{'loop'};
  293. # Do not pass GO, do not collect $200
  294. return 1;
  295. }
  296. sub lsr_loop($$$) {
  297. my ($name, $exists, $params) = @_;
  298. if ($DEBUG) {
  299. print STDERR "lsr_loop()\n";
  300. }
  301. # Config
  302. my $max_dur = 375;
  303. my $max_val = 255;
  304. my $reserve = 0.75;
  305. my $ramp_dur = 10.20;
  306. my $hit_pos = 43.15;
  307. my $hit_dur = 100;
  308. my $fade_pos = 43.65;
  309. my $fade_dur = 2000;
  310. # How long have we been playing
  311. my $elapsed = Time::HiRes::time() - $PID_DATA->{'start'};
  312. if ($DEBUG) {
  313. print STDERR 'Elapsed: ' . $elapsed . "\n";
  314. }
  315. # Ramp up the number of channels in our effect
  316. if ($elapsed < $ramp_dur) {
  317. my $ratio = $PID_DATA->{'live_channels'} / ($PID_DATA->{'num_channels'} * (1 - $reserve));
  318. if ($PID_DATA->{'live_channels'} < 1 || $ratio < $elapsed / $ramp_dur) {
  319. my @keys = keys(%{ $PID_DATA->{'channels'} });
  320. my $index = int(rand($PID_DATA->{'num_channels'}));
  321. while ($PID_DATA->{'channels'}->{ $keys[$index] } > 0) {
  322. $index = int(rand($PID_DATA->{'num_channels'}));
  323. }
  324. $PID_DATA->{'channels'}->{ $keys[$index] } = 1;
  325. $PID_DATA->{'live_channels'}++;
  326. }
  327. } else {
  328. if ($PID_DATA->{'live_channels'} < $PID_DATA->{'num_channels'}) {
  329. foreach my $chan (keys(%{ $PID_DATA->{'channels'} })) {
  330. $PID_DATA->{'channels'}->{$chan} = 1;
  331. }
  332. $PID_DATA->{'live_channels'} = $PID_DATA->{'num_channels'};
  333. }
  334. }
  335. # Prepare data for each channel
  336. my @data_set = ();
  337. my $wait = $max_dur;
  338. foreach my $chan (keys(%{ $PID_DATA->{'channels'} })) {
  339. my $val = 0;
  340. my $dur = 0;
  341. # Random data on enabled channels until the fade
  342. if ($elapsed < $hit_pos) {
  343. if ($PID_DATA->{'channels'}->{$chan} > 0) {
  344. $dur = int(rand($max_dur));
  345. $val = int(rand($max_val));
  346. }
  347. } elsif ($elapsed < $fade_pos) {
  348. $val = $max_val;
  349. $dur = $hit_dur;
  350. $wait = ($fade_pos - $hit_pos) * 1000;
  351. } else {
  352. $val = 0;
  353. $dur = $fade_dur;
  354. $wait = $dur;
  355. }
  356. push(@data_set, { 'channel' => $chan, 'value' => $val, 'time' => $dur });
  357. }
  358. # Push the data set
  359. DMX::applyDataset(\@data_set, 'RAVE', $OUTPUT_FILE);
  360. # Wait for the fade interval
  361. usleep($wait * 1000);
  362. }