Add support for 4-pin range sensors, the velocity sensor, and LED status.

FossilOrigin-Name: ab34614152b3a25488aa4b55c45ec4a6f625bde42cd794209464825bb7654040
This commit is contained in:
Mahlon E. Smith 2023-05-12 05:46:35 +00:00
parent 07b2d81523
commit 5a4fcbae87
4 changed files with 124 additions and 43 deletions

View file

@ -1,18 +1,27 @@
# -*- ruby -*- # -*- yaml -*-
# vim: set et sta sw=2 : # vim: set et sta sw=2 :
--- ---
logging: logging:
__default__: info (color) __default__: info (color)
theremin: debug (color) theremin: debug (color)
# gpio: debug (color)
# See: https://pinout.xyz/
#
theremin: theremin:
pitch_trigger: 16 pitch_trigger: 27
font: /usr/share/sounds/sf2/FluidR3_GM.sf2 pitch_echo: 22
glide: 1 velocity_trigger: 23
slew: 0 velocity_echo: 24
sample_interval: 0.1 distance_max: 2
reverb: false cancel_after: 3
chorus: false # font: /usr/share/sounds/sf2/FluidR3_GM.sf2
instrument_index: 29 # glide: 1
# slew: 5
# sample_interval: 0.5
# reverb: false
# chorus: false
# font_index: 2
# instrument_index: 29

View file

@ -48,11 +48,13 @@ class FluidSynth
end end
### Instance a new fluidsynth interface.
###
def initialize( host=DEFAULT_HOST, port=DEFAULT_PORT ) def initialize( host=DEFAULT_HOST, port=DEFAULT_PORT )
@host = host @host = host
@port = port @port = port
@lastnote = nil @lastnote = nil
@velocity = 80 @velocity = 0
end end
# The TCP socket to fluidsynth. # The TCP socket to fluidsynth.
@ -96,6 +98,14 @@ class FluidSynth
end 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 ### Turn glide (portamento) on or off. Val is an integer that
### represents 128ms. 4 == 512ms. ### represents 128ms. 4 == 512ms.
### ###
@ -132,13 +142,6 @@ class FluidSynth
end 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+. ### Loads a resource (sf2, etc) from +path+.
### ###
def load( path ) def load( path )

View file

@ -37,7 +37,7 @@ class GPIO
value: BASE + "gpio#{pin}" + "value" value: BASE + "gpio#{pin}" + "value"
} }
self.export rescue Errno::EBUSY nil self.export rescue nil
end end
# The GPIO PIN number. # The GPIO PIN number.

View file

@ -1,6 +1,7 @@
# -*- ruby -*- # -*- ruby -*-
# vim: set noet sta sw=4 ts=4 : # vim: set noet sta sw=4 ts=4 :
require 'pathname'
require 'forwardable' require 'forwardable'
require 'configurability' require 'configurability'
require 'loggability' require 'loggability'
@ -19,6 +20,10 @@ class Theremin
# The speed of sound, in centmeters/sec. # 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 # Loggability API
log_as :theremin log_as :theremin
@ -62,6 +67,7 @@ class Theremin
# The soundfont to use for the synth. Will look in the # The soundfont to use for the synth. Will look in the
# current working directory by default, but absolute paths # current working directory by default, but absolute paths
# can be used for system sounds. # can be used for system sounds.
# The "Theremin.sf2" soundfont is always loaded into index 1!
setting :font, default: 'Theremin.sf2' do |val| setting :font, default: 'Theremin.sf2' do |val|
val = Pathname( val ) val = Pathname( val )
val = Pathname.pwd + val unless val.absolute? val = Pathname.pwd + val unless val.absolute?
@ -75,7 +81,7 @@ class Theremin
## ##
# A reasonable distance in feet for the theremin to map notes. # A reasonable distance in feet for the theremin to map notes.
setting :distance_max, default: 4 setting :distance_max, default: 3
## ##
# Initial synth volume. # Initial synth volume.
@ -90,7 +96,11 @@ class Theremin
setting :chorus, default: true 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 setting :instrument_index, default: 1
## ##
@ -118,16 +128,14 @@ class Theremin
### Instance a new Theremin daemon. ### Instance a new Theremin daemon.
### ###
def initialize def initialize
# TODO: @pins = %i[ pitch_trigger pitch_echo velocity_trigger velocity_echo ] @pins = %i[ pitch_trigger pitch_echo velocity_trigger velocity_echo ]
@pins = %i[ pitch_trigger ]
self.setup_gpio
@timeout = 0 @timeout = 0
@synth = FluidSynth.new( self.class.host, self.class.port ) @synth = FluidSynth.new( self.class.host, self.class.port )
@running = false @running = false
@threads = [] @threads = []
self.setup_gpio
rescue => err rescue => err
self.log.warn( err.message ) self.log.warn( err.message )
self.stop rescue nil self.stop rescue nil
@ -154,12 +162,15 @@ class Theremin
self.gain( self.class.gain ) self.gain( self.class.gain )
self.glide( self.class.glide ) 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.reverb( self.class.reverb )
self.chorus( self.class.chorus ) 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.note_thread
@threads << self.velocity_thread
self.led( :green )
self.log.warn "Waiting for interaction, or ctrl-c to exit." self.log.warn "Waiting for interaction, or ctrl-c to exit."
@threads.map( &:join ) @threads.map( &:join )
end end
@ -171,6 +182,7 @@ class Theremin
@running = false @running = false
self.pins.each {|pin| self.class.send( pin ).unexport rescue nil } self.pins.each {|pin| self.class.send( pin ).unexport rescue nil }
self.synth.socket.close self.synth.socket.close
self.led( :red )
end end
@ -187,12 +199,12 @@ class Theremin
] ]
return self.running_thread do return self.running_thread do
distance = self.distance distance = self.distance( :pitch )
note = self.distance_to_note( distance ) note = self.distance_to_note( distance )
# Only play if within maximum 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 ] self.log.debug "Pitch distance %0.2f (note %s)" % [ distance, note ]
# Hold the note if it is unchanged within slew amount. # Hold the note if it is unchanged within slew amount.
@ -218,6 +230,32 @@ class Theremin
end 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. ### Create a new thread loop that returns when @running is false.
### ###
def running_thread( &block ) def running_thread( &block )
@ -239,9 +277,12 @@ class Theremin
raise "GPIO pins are required configuration options: %p" % [ self.pins ] raise "GPIO pins are required configuration options: %p" % [ self.pins ]
end end
self.class.pitch_trigger.edge( :both ) self.class.pitch_trigger.mode( :out )
# self.class.pitch_echo.edge( :both ) self.class.velocity_trigger.mode( :out )
# TODO: self.class.velocity_echo.edge( :both ) 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 end
@ -258,22 +299,35 @@ class Theremin
end end
### TODO: make generic for a trigger/echo pin pair ### For a given +distance+ in feet, convert into a velocity range (0-127).
### Returns detected distance in feet.
### ###
def distance def distance_to_velocity( distance )
self.class.pitch_trigger.mode( :out ) velocity_start = 127
self.class.pitch_trigger.write( 1 ) 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 sleep 0.00001
self.class.pitch_trigger.write( 0 ) device_trigger.write( 0 )
self.class.pitch_trigger.mode( :in )
duration = Time.now duration = Time.now
while self.class.pitch_trigger.read == 0 while device_echo.read == 0
starttime = Time.now starttime = Time.now
break if starttime - duration > GPIO::TIMEOUT break if starttime - duration > GPIO::TIMEOUT
end end
while self.class.pitch_trigger.read == 1 while device_echo.read == 1
stoptime = Time.now stoptime = Time.now
break if stoptime - duration > GPIO::TIMEOUT break if stoptime - duration > GPIO::TIMEOUT
end end
@ -289,5 +343,20 @@ class Theremin
rescue rescue
return 0 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
end # class Theremin