From 7bc47cfc4d62a4e946b8b33c8c009b78666d18ae Mon Sep 17 00:00:00 2001 From: mahlon Date: Fri, 12 May 2023 06:44:09 +0000 Subject: [PATCH] Setup espeak TTS. Fix "velocity" -> "gain". I was originally thinking the velocity sensor would adjust the midi note velocity. The downside to this is that you have to wait for a note to play to "feel" the volume change. When changed to alter the gain of the synth itself, it is much smoother -- works for any sound, even those that have already started playing. FossilOrigin-Name: 9b5a893d5b6e3aed1e11d0b984b5f498d9b67f55cda304bbef4179944ca6260f --- README.md | 2 +- theremin/config.yml | 4 +- theremin/lib/fluidsynth.rb | 2 +- theremin/lib/theremin.rb | 85 ++++++++++++++++++++++++-------------- 4 files changed, 58 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 18d992d..8182bd4 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ 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: - # apt install ruby fluidsynth runit + # apt install ruby espeak fluidsynth runit # gem install -Ng # ln -s /path/to/here/theremin /etc/service # ln -s /path/to/here/fluidsynth /etc/service diff --git a/theremin/config.yml b/theremin/config.yml index 6981cf0..94ad2fd 100644 --- a/theremin/config.yml +++ b/theremin/config.yml @@ -12,8 +12,8 @@ logging: theremin: pitch_trigger: 27 pitch_echo: 22 - velocity_trigger: 23 - velocity_echo: 24 + gain_trigger: 23 + gain_echo: 24 distance_max: 2 cancel_after: 3 # font: /usr/share/sounds/sf2/FluidR3_GM.sf2 diff --git a/theremin/lib/fluidsynth.rb b/theremin/lib/fluidsynth.rb index 6f88817..554fda3 100644 --- a/theremin/lib/fluidsynth.rb +++ b/theremin/lib/fluidsynth.rb @@ -54,7 +54,7 @@ class FluidSynth @host = host @port = port @lastnote = nil - @velocity = 0 + @velocity = 80 end # The TCP socket to fluidsynth. diff --git a/theremin/lib/theremin.rb b/theremin/lib/theremin.rb index 6243abc..a61f68a 100755 --- a/theremin/lib/theremin.rb +++ b/theremin/lib/theremin.rb @@ -7,6 +7,7 @@ require 'configurability' require 'loggability' require 'gpio' require 'fluidsynth' +require 'open3' # A class that coordinates GPIO events and translates @@ -52,14 +53,14 @@ class Theremin end ## - # The GPIO pin the velocity trigger is on. - setting :velocity_trigger do |val| + # The GPIO pin the gain trigger is on. + setting :gain_trigger do |val| GPIO.new( val ) if val end ## - # The GPIO pin the velocity echo measurement is on. - setting :velocity_echo do |val| + # The GPIO pin the gain echo measurement is on. + setting :gain_echo do |val| GPIO.new( val ) if val end @@ -85,7 +86,7 @@ class Theremin ## # Initial synth volume. - setting :gain, default: 1.0 + setting :gain, default: 0.0 ## # Enable reverb? @@ -128,12 +129,14 @@ class Theremin ### Instance a new Theremin daemon. ### def initialize - @pins = %i[ pitch_trigger pitch_echo velocity_trigger velocity_echo ] - @timeout = 0 - @synth = FluidSynth.new( self.class.host, self.class.port ) - @running = false - @threads = [] + @pins = %i[ pitch_trigger pitch_echo gain_trigger gain_echo ] + @timeout = 0 + @synth = FluidSynth.new( self.class.host, self.class.port ) + @speech_io = nil + @running = false + @threads = [] + self.setup_speech self.setup_gpio rescue => err @@ -150,6 +153,9 @@ class Theremin # The Fluidsynth instance. attr_reader :synth + # An IO to stream to espeak. + attr_reader :speech_io + # Is this Theremin currently running? attr_reader :running @@ -169,8 +175,9 @@ class Theremin self.instrument( self.class.font_index, self.class.instrument_index ) @threads << self.note_thread - @threads << self.velocity_thread + @threads << self.gain_thread self.led( :green ) + self.speak "Hi. I'm Maude." self.log.warn "Waiting for interaction, or ctrl-c to exit." @threads.map( &:join ) end @@ -182,6 +189,7 @@ class Theremin @running = false self.pins.each {|pin| self.class.send( pin ).unexport rescue nil } self.synth.socket.close + self.speech_io.close if self.speech_io && !self.speech_io.closed? self.led( :red ) end @@ -230,25 +238,25 @@ class Theremin end - ### Measure velocity distance, convert for synth notes. + ### Measure gain distance, convert for any playing notes. ### - def velocity_thread - self.log.info "Starting velocity measurements (every %s/sec) within %d feet." % [ + def gain_thread + self.log.info "Starting gain 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 ) + distance = self.distance( :gain ) + gain = self.distance_to_gain( 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 + self.log.debug "Gain distance %0.2f (gain %s)" % [ distance, gain ] + synth.gain( gain ) else - synth.velocity = 0 + synth.gain( 0 ) end sleep self.class.sample_interval @@ -256,6 +264,15 @@ class Theremin end + ### Open a pipe to espeak, and wait on input. + ## + def setup_speech + self.log.info "Starting background espeak process." + cmd = %w[ espeak -k20 -v f5 -s 160 -a 150 ] + @speech_io, stdout, stderr, wait_thr = Open3.popen3( *cmd ) + end + + ### Create a new thread loop that returns when @running is false. ### def running_thread( &block ) @@ -278,11 +295,11 @@ class Theremin end self.class.pitch_trigger.mode( :out ) - self.class.velocity_trigger.mode( :out ) + self.class.gain_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 ) + self.class.gain_echo.mode( :in ) + self.class.gain_echo.edge( :both ) end @@ -295,23 +312,22 @@ class Theremin distance_min = 0 slope = 1.0 * ( midi_end - midi_start ) / ( self.class.distance_max - distance_min ) - return midi_start + ( slope * ( distance - distance_min ) ).round + return ( midi_start + ( slope * ( distance - distance_min ) ) ).round end - ### For a given +distance+ in feet, convert into a velocity range (0-127). + ### For a given +distance+ in feet, convert into a gain range (0-5). ### - def distance_to_velocity( distance ) - velocity_start = 127 - velocity_end = 1 - distance_min = 0 + def distance_to_gain( distance ) + gain_start = 5.0 + gain_end = 0.0 + distance_min = 0 - slope = 1.0 * ( velocity_end - velocity_start ) / ( self.class.distance_max - distance_min ) - return velocity_start + ( slope * ( distance - distance_min ) ).round + slope = 1.0 * ( gain_end - gain_start ) / ( self.class.distance_max - distance_min ) + return ( gain_start + ( slope * ( distance - distance_min ) ) ).round( 2 ) end - ### Returns detected distance in feet for a given +device+. ### def distance( device ) @@ -358,5 +374,12 @@ class Theremin end end + + ### Send text to the speech engine. + ### + def speak( phrase ) + self.speech_io.puts( phrase ) + end + end # class Theremin