From 5a4fcbae870e0478b7b25e460700b58558625a33 Mon Sep 17 00:00:00 2001 From: mahlon Date: Fri, 12 May 2023 05:46:35 +0000 Subject: [PATCH] Add support for 4-pin range sensors, the velocity sensor, and LED status. FossilOrigin-Name: ab34614152b3a25488aa4b55c45ec4a6f625bde42cd794209464825bb7654040 --- theremin/config.yml | 27 ++++++--- theremin/lib/fluidsynth.rb | 19 +++--- theremin/lib/gpio.rb | 2 +- theremin/lib/theremin.rb | 119 +++++++++++++++++++++++++++++-------- 4 files changed, 124 insertions(+), 43 deletions(-) diff --git a/theremin/config.yml b/theremin/config.yml index 3131140..6981cf0 100644 --- a/theremin/config.yml +++ b/theremin/config.yml @@ -1,18 +1,27 @@ -# -*- ruby -*- +# -*- yaml -*- # vim: set et sta sw=2 : --- logging: __default__: info (color) theremin: debug (color) +# gpio: debug (color) +# See: https://pinout.xyz/ +# theremin: - pitch_trigger: 16 - font: /usr/share/sounds/sf2/FluidR3_GM.sf2 - glide: 1 - slew: 0 - sample_interval: 0.1 - reverb: false - chorus: false - instrument_index: 29 + pitch_trigger: 27 + pitch_echo: 22 + velocity_trigger: 23 + velocity_echo: 24 + distance_max: 2 + cancel_after: 3 +# font: /usr/share/sounds/sf2/FluidR3_GM.sf2 +# glide: 1 +# slew: 5 +# sample_interval: 0.5 +# reverb: false +# chorus: false +# font_index: 2 +# instrument_index: 29 diff --git a/theremin/lib/fluidsynth.rb b/theremin/lib/fluidsynth.rb index d6fd84c..6f88817 100644 --- a/theremin/lib/fluidsynth.rb +++ b/theremin/lib/fluidsynth.rb @@ -48,11 +48,13 @@ class FluidSynth end + ### Instance a new fluidsynth interface. + ### def initialize( host=DEFAULT_HOST, port=DEFAULT_PORT ) @host = host @port = port @lastnote = nil - @velocity = 80 + @velocity = 0 end # The TCP socket to fluidsynth. @@ -96,6 +98,14 @@ class FluidSynth end + ### Select a loaded soundfont and instrument by index. + ### (Always using channel 0.) + ### + def instrument( font_idx, instrument_idx ) + self.send "select 0 #{font_idx} 0 #{instrument_idx}" + end + + ### Turn glide (portamento) on or off. Val is an integer that ### represents 128ms. 4 == 512ms. ### @@ -132,13 +142,6 @@ class FluidSynth end - ### Selects an instrument +index+ within the currently loaded sf2 for channel 0. - ### - def instrument( index ) - self.send "select 0 1 0 #{index}" - end - - ### Loads a resource (sf2, etc) from +path+. ### def load( path ) diff --git a/theremin/lib/gpio.rb b/theremin/lib/gpio.rb index 95cca86..a0a9db7 100755 --- a/theremin/lib/gpio.rb +++ b/theremin/lib/gpio.rb @@ -37,7 +37,7 @@ class GPIO value: BASE + "gpio#{pin}" + "value" } - self.export rescue Errno::EBUSY nil + self.export rescue nil end # The GPIO PIN number. diff --git a/theremin/lib/theremin.rb b/theremin/lib/theremin.rb index 70fb766..6243abc 100755 --- a/theremin/lib/theremin.rb +++ b/theremin/lib/theremin.rb @@ -1,6 +1,7 @@ # -*- ruby -*- # vim: set noet sta sw=4 ts=4 : +require 'pathname' require 'forwardable' require 'configurability' require 'loggability' @@ -17,7 +18,11 @@ class Theremin Forwardable # The speed of sound, in centmeters/sec. - SONICSPEED = 34300 + SONICSPEED = 34300 + + # The soundfont that is loaded to index 1. + DEFAULT_FONT = Pathname.pwd + 'Theremin.sf2' + # Loggability API log_as :theremin @@ -62,6 +67,7 @@ class Theremin # The soundfont to use for the synth. Will look in the # current working directory by default, but absolute paths # can be used for system sounds. + # The "Theremin.sf2" soundfont is always loaded into index 1! setting :font, default: 'Theremin.sf2' do |val| val = Pathname( val ) val = Pathname.pwd + val unless val.absolute? @@ -75,7 +81,7 @@ class Theremin ## # A reasonable distance in feet for the theremin to map notes. - setting :distance_max, default: 4 + setting :distance_max, default: 3 ## # Initial synth volume. @@ -90,7 +96,11 @@ class Theremin setting :chorus, default: true ## - # What instrument to use from the loaded SF2. + # What font to initially use. 1 is the theremin. 2 is user loaded. + setting :font_index, default: 1 + + ## + # What instrument to initially use from the loaded SF2. setting :instrument_index, default: 1 ## @@ -118,16 +128,14 @@ class Theremin ### Instance a new Theremin daemon. ### def initialize - # TODO: @pins = %i[ pitch_trigger pitch_echo velocity_trigger velocity_echo ] - @pins = %i[ pitch_trigger ] - - self.setup_gpio - + @pins = %i[ pitch_trigger pitch_echo velocity_trigger velocity_echo ] @timeout = 0 @synth = FluidSynth.new( self.class.host, self.class.port ) @running = false @threads = [] + self.setup_gpio + rescue => err self.log.warn( err.message ) self.stop rescue nil @@ -154,12 +162,15 @@ class Theremin self.gain( self.class.gain ) self.glide( self.class.glide ) - self.load( self.class.font.to_s ) + self.load( DEFAULT_FONT ) + self.load( self.class.font.to_s ) unless self.class.font == DEFAULT_FONT self.reverb( self.class.reverb ) self.chorus( self.class.chorus ) - self.instrument( self.class.instrument_index ) + self.instrument( self.class.font_index, self.class.instrument_index ) @threads << self.note_thread + @threads << self.velocity_thread + self.led( :green ) self.log.warn "Waiting for interaction, or ctrl-c to exit." @threads.map( &:join ) end @@ -171,6 +182,7 @@ class Theremin @running = false self.pins.each {|pin| self.class.send( pin ).unexport rescue nil } self.synth.socket.close + self.led( :red ) end @@ -187,12 +199,12 @@ class Theremin ] return self.running_thread do - distance = self.distance + distance = self.distance( :pitch ) note = self.distance_to_note( distance ) # Only play if within maximum distance. # - if distance <= self.class.distance_max + if distance > 0 && distance <= self.class.distance_max self.log.debug "Pitch distance %0.2f (note %s)" % [ distance, note ] # Hold the note if it is unchanged within slew amount. @@ -218,6 +230,32 @@ class Theremin end + ### Measure velocity distance, convert for synth notes. + ### + def velocity_thread + self.log.info "Starting velocity measurements (every %s/sec) within %d feet." % [ + self.class.sample_interval, + self.class.distance_max + ] + + return self.running_thread do + distance = self.distance( :velocity ) + velocity = self.distance_to_velocity( distance ) + + # Only set if within maximum distance. + # + if distance > 0 && distance <= self.class.distance_max + self.log.debug "Velocity distance %0.2f (velocity %s)" % [ distance, velocity ] + synth.velocity = velocity + else + synth.velocity = 0 + end + + sleep self.class.sample_interval + end + end + + ### Create a new thread loop that returns when @running is false. ### def running_thread( &block ) @@ -239,9 +277,12 @@ class Theremin raise "GPIO pins are required configuration options: %p" % [ self.pins ] end - self.class.pitch_trigger.edge( :both ) - # self.class.pitch_echo.edge( :both ) - # TODO: self.class.velocity_echo.edge( :both ) + self.class.pitch_trigger.mode( :out ) + self.class.velocity_trigger.mode( :out ) + self.class.pitch_echo.mode( :in ) + self.class.pitch_echo.edge( :both ) + self.class.velocity_echo.mode( :in ) + self.class.velocity_echo.edge( :both ) end @@ -258,22 +299,35 @@ class Theremin end - ### TODO: make generic for a trigger/echo pin pair - ### Returns detected distance in feet. + ### For a given +distance+ in feet, convert into a velocity range (0-127). ### - def distance - self.class.pitch_trigger.mode( :out ) - self.class.pitch_trigger.write( 1 ) + def distance_to_velocity( distance ) + velocity_start = 127 + velocity_end = 1 + distance_min = 0 + + slope = 1.0 * ( velocity_end - velocity_start ) / ( self.class.distance_max - distance_min ) + return velocity_start + ( slope * ( distance - distance_min ) ).round + end + + + + ### Returns detected distance in feet for a given +device+. + ### + def distance( device ) + device_trigger = self.class.send( "#{device}_trigger".to_sym ) + device_echo = self.class.send( "#{device}_echo".to_sym ) + + device_trigger.write( 1 ) sleep 0.00001 - self.class.pitch_trigger.write( 0 ) - self.class.pitch_trigger.mode( :in ) + device_trigger.write( 0 ) duration = Time.now - while self.class.pitch_trigger.read == 0 + while device_echo.read == 0 starttime = Time.now break if starttime - duration > GPIO::TIMEOUT end - while self.class.pitch_trigger.read == 1 + while device_echo.read == 1 stoptime = Time.now break if stoptime - duration > GPIO::TIMEOUT end @@ -289,5 +343,20 @@ class Theremin rescue return 0 end -end + + + ### Set the LED. Maybe it'll be visible. + ### + 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) } + 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) } + end + end + +end # class Theremin