diff --git a/README.md b/README.md index 8182bd4..e6a1169 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # RPi Theremin -This is a quick-n-dirty Theremin daemon for use on the RaspberryPi. +This is a quick-n-dirty "Theremin" daemon for use on the RaspberryPi. It is designed to be used with a couple of these distance sensors: @@ -9,12 +9,25 @@ It is designed to be used with a couple of these distance sensors: Use 'raspi-config' to configure your default sound output (HDMI, 3.5", etc). +This is intended to be run from daemontools or runit. Quick setup as root: -This is intended to be run from daemontools or runit. Quick setup: - - # apt install ruby espeak fluidsynth runit + # apt install ruby espeak fluidsynth runit motion netcat + # systemctl stop/disable/mask motion # gem install -Ng + # ln -s /etc/service /service # ln -s /path/to/here/theremin /etc/service + # ln -s /path/to/here/motion /etc/motion # ln -s /path/to/here/fluidsynth /etc/service +For this project, there was an available Pi camera (v2). After enabling it in +raspi-config, I also fired up 'motion' with the included configuration file, to +capture people using the theremin and to say hello. + + # ln -s /path/to/here/motion /etc/service + # (enable motion support in the config file) + +It will save images for 2 weeks with the current motion.conf. Season to taste. + +This was mounted on a manniquin named "Maude" (the bod), as a goofy/fun +interactive instrument. diff --git a/motion/motion.conf b/motion/motion.conf new file mode 100644 index 0000000..6e259df --- /dev/null +++ b/motion/motion.conf @@ -0,0 +1,50 @@ +daemon off +setup_mode off +log_level 6 +log_file /dev/null + +stream_localhost off +stream_port 8080 +stream_motion on +stream_preview_method 4 + +videodevice /dev/video0 +camera_name MaudeCam +camera_id 1 +width 1024 +height 768 + +locate_motion_mode on +framerate 5 +text_left MAUDECAM!! +text_changes on +text_scale 3 +text_right %Y-%m-%d\n%T-%q +emulate_motion off +threshold 2500 # Threshold for number of changed pixels that triggers motion. +threshold_maximum 2050000 +lightswitch_percent 40 +lightswitch_frames 15 +; noise_level 32 # Noise threshold for the motion detection. +despeckle_filter EedDl +minimum_motion_frames 3 # Number of images that must contain motion to trigger an event. +event_gap 30 # Gap in seconds of no motion detected that triggers the end of an event. +pre_capture 2 # The number of pre-captured (buffered) pictures from before motion. +post_capture 0 # Number of frames to capture after motion is no longer detected. + +on_event_start echo 1 | nc localhost 62877 +on_event_end find /service/motion/capture -type f -mtime +12 -depth 1 -delete + +picture_output first +picture_filename capture/%Y%m%d%H%M%S-%q + +#timelapse_mode weekly +#timelapse_interval 1800 +#timelapse_filename timelapse/%Y%m%d + +movie_output off +#movie_max_time 60 # Maximum length of movie in seconds. +#movie_quality 0 # The encoding quality of the movie. (0=use bitrate. 1=worst quality, 100=best) +#movie_codec mkv +#movie_filename motion/%t-%v-%Y%m%d%H%M%S + diff --git a/motion/run b/motion/run new file mode 100755 index 0000000..85bcca3 --- /dev/null +++ b/motion/run @@ -0,0 +1,5 @@ +#!/bin/sh + +exec 2>&1 +exec /usr/bin/motion -c /service/motion/motion.conf + diff --git a/theremin/config.yml b/theremin/config.yml index e611490..da2ab59 100644 --- a/theremin/config.yml +++ b/theremin/config.yml @@ -4,23 +4,35 @@ --- logging: __default__: info (color) - theremin: debug (color) +# theremin: debug (color) # gpio: debug (color) -# See: https://pinout.xyz/ -# theremin: + # See: https://pinout.xyz/ - GPIO labels, not physical pitch_trigger: 27 pitch_echo: 22 gain_trigger: 23 gain_echo: 24 + + # Fine tune range sensors distance_max: 2 + # sample_interval: 0.5 + # slew: 5 + + # Speech + startup_message: Hello. I'm Maude the bod. + use_motion: true + motion_messages: + - Lets make beautiful music together. + - Oh yeah! + - Yes yes yes yes yessss yyyyyyyyyyyyyyyyyyyyyyyy yes! + - oooooooooooooooooooooookay! + - Let's rock. + + # Alter synth sounds from defaults. cancel_after: 3 - startup_message: Hello. I'm Maude the bod. Lets make beautiful music together. # font: /usr/share/sounds/sf2/FluidR3_GM.sf2 # glide: 1 -# slew: 5 -# sample_interval: 0.5 # reverb: false # chorus: false # font_index: 2 diff --git a/theremin/lib/gpio.rb b/theremin/lib/gpio.rb old mode 100755 new mode 100644 diff --git a/theremin/lib/theremin.rb b/theremin/lib/theremin.rb old mode 100755 new mode 100644 index e0437d3..fde591e --- a/theremin/lib/theremin.rb +++ b/theremin/lib/theremin.rb @@ -8,6 +8,7 @@ require 'loggability' require 'gpio' require 'fluidsynth' require 'open3' +require 'socket' # A class that coordinates GPIO events and translates @@ -130,6 +131,12 @@ class Theremin ## ESpeak TTL options. setting :espeak_flags, default: %w[ -k20 -v f5 -s 160 -a 150 ] + + ## Watch for camera motion? + setting :use_motion, default: false + + ## A list of phrases to randomly say when motion is detected. + setting :motion_messages, default: [] end @@ -163,6 +170,9 @@ class Theremin # An IO to stream to espeak. attr_reader :speech_io + # A TCPServer for motion IPC + attr_reader :motion_socket + # Is this Theremin currently running? attr_reader :running @@ -181,10 +191,11 @@ class Theremin self.chorus( self.class.chorus ) self.instrument( self.class.font_index, self.class.instrument_index ) + self.speak( self.class.startup_message ) if self.class.startup_message @threads << self.note_thread @threads << self.gain_thread + @threads << self.motion_thread if self.class.use_motion self.led( :green ) - self.speak( self.class.startup_message ) if self.class.startup_message self.log.warn "Waiting for interaction, or ctrl-c to exit." @threads.map( &:join ) end @@ -196,14 +207,15 @@ class Theremin @running = false self.pins.each {|pin| self.class.send( pin ).unexport rescue nil } self.synth.socket.close + self.motion_socket.close self.speech_io.close if self.speech_io && !self.speech_io.closed? self.led( :red ) end - - ######### - protected - ######### + + ######### + protected + ######### ### Measure pitch distance, and manage notes sent to the synth. ### @@ -271,6 +283,23 @@ class Theremin end + ### Watch the motion fifo, say a random phrase when motion seen. + ### + def motion_thread + phrases = self.class.motion_messages + return if phrases.empty? + + @motion_socket = TCPServer.new( 62877 ) + return self.running_thread do + client = @motion_socket.accept + self.log.info "Camera motion detected." + client.close + self.speak( phrases.sample ) + rescue IOError + end + end + + ### Open a pipe to espeak, and wait on input. ## def setup_speech @@ -375,11 +404,11 @@ class Theremin def led( color ) case color when :red - File.open( '/sys/class/leds/led0/brightness', 'w' ){|l| l.puts(0) } - File.open( '/sys/class/leds/led1/brightness', 'w' ){|l| l.puts(1) } + File.open( '/sys/class/leds/PWR/brightness', 'w' ){|l| l.puts(1) } + File.open( '/sys/class/leds/ACT/brightness', 'w' ){|l| l.puts(0) } when :green - File.open( '/sys/class/leds/led0/brightness', 'w' ){|l| l.puts(1) } - File.open( '/sys/class/leds/led1/brightness', 'w' ){|l| l.puts(0) } + File.open( '/sys/class/leds/PWR/brightness', 'w' ){|l| l.puts(0) } + File.open( '/sys/class/leds/ACT/brightness', 'w' ){|l| l.puts(1) } end end diff --git a/theremin/log/main/.placeholder b/theremin/log/main/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/theremin/log/run b/theremin/log/run new file mode 100755 index 0000000..8e0c408 --- /dev/null +++ b/theremin/log/run @@ -0,0 +1,5 @@ +#!/bin/sh + +exec 2>&1 +exec svlogd ./main + diff --git a/theremin/run b/theremin/run index 6598790..2e11d0c 100755 --- a/theremin/run +++ b/theremin/run @@ -15,5 +15,7 @@ Signal.trap( "INT" ) do theremin.stop if theremin.running end +$stderr.sync = true +$stdout.sync = true theremin.start