rave.pl 12 KB

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