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
This commit is contained in:
Mahlon E. Smith 2023-05-12 06:44:09 +00:00
parent 5a4fcbae87
commit 7bc47cfc4d
4 changed files with 58 additions and 35 deletions

View file

@ -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: 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 # gem install -Ng
# ln -s /path/to/here/theremin /etc/service # ln -s /path/to/here/theremin /etc/service
# ln -s /path/to/here/fluidsynth /etc/service # ln -s /path/to/here/fluidsynth /etc/service

View file

@ -12,8 +12,8 @@ logging:
theremin: theremin:
pitch_trigger: 27 pitch_trigger: 27
pitch_echo: 22 pitch_echo: 22
velocity_trigger: 23 gain_trigger: 23
velocity_echo: 24 gain_echo: 24
distance_max: 2 distance_max: 2
cancel_after: 3 cancel_after: 3
# font: /usr/share/sounds/sf2/FluidR3_GM.sf2 # font: /usr/share/sounds/sf2/FluidR3_GM.sf2

View file

@ -54,7 +54,7 @@ class FluidSynth
@host = host @host = host
@port = port @port = port
@lastnote = nil @lastnote = nil
@velocity = 0 @velocity = 80
end end
# The TCP socket to fluidsynth. # The TCP socket to fluidsynth.

View file

@ -7,6 +7,7 @@ require 'configurability'
require 'loggability' require 'loggability'
require 'gpio' require 'gpio'
require 'fluidsynth' require 'fluidsynth'
require 'open3'
# A class that coordinates GPIO events and translates # A class that coordinates GPIO events and translates
@ -52,14 +53,14 @@ class Theremin
end end
## ##
# The GPIO pin the velocity trigger is on. # The GPIO pin the gain trigger is on.
setting :velocity_trigger do |val| setting :gain_trigger do |val|
GPIO.new( val ) if val GPIO.new( val ) if val
end end
## ##
# The GPIO pin the velocity echo measurement is on. # The GPIO pin the gain echo measurement is on.
setting :velocity_echo do |val| setting :gain_echo do |val|
GPIO.new( val ) if val GPIO.new( val ) if val
end end
@ -85,7 +86,7 @@ class Theremin
## ##
# Initial synth volume. # Initial synth volume.
setting :gain, default: 1.0 setting :gain, default: 0.0
## ##
# Enable reverb? # Enable reverb?
@ -128,12 +129,14 @@ class Theremin
### Instance a new Theremin daemon. ### Instance a new Theremin daemon.
### ###
def initialize def initialize
@pins = %i[ pitch_trigger pitch_echo velocity_trigger velocity_echo ] @pins = %i[ pitch_trigger pitch_echo gain_trigger gain_echo ]
@timeout = 0 @timeout = 0
@synth = FluidSynth.new( self.class.host, self.class.port ) @synth = FluidSynth.new( self.class.host, self.class.port )
@speech_io = nil
@running = false @running = false
@threads = [] @threads = []
self.setup_speech
self.setup_gpio self.setup_gpio
rescue => err rescue => err
@ -150,6 +153,9 @@ class Theremin
# The Fluidsynth instance. # The Fluidsynth instance.
attr_reader :synth attr_reader :synth
# An IO to stream to espeak.
attr_reader :speech_io
# Is this Theremin currently running? # Is this Theremin currently running?
attr_reader :running attr_reader :running
@ -169,8 +175,9 @@ class Theremin
self.instrument( self.class.font_index, 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 @threads << self.gain_thread
self.led( :green ) self.led( :green )
self.speak "Hi. I'm Maude."
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
@ -182,6 +189,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.speech_io.close if self.speech_io && !self.speech_io.closed?
self.led( :red ) self.led( :red )
end end
@ -230,25 +238,25 @@ class Theremin
end end
### Measure velocity distance, convert for synth notes. ### Measure gain distance, convert for any playing notes.
### ###
def velocity_thread def gain_thread
self.log.info "Starting velocity measurements (every %s/sec) within %d feet." % [ self.log.info "Starting gain measurements (every %s/sec) within %d feet." % [
self.class.sample_interval, self.class.sample_interval,
self.class.distance_max self.class.distance_max
] ]
return self.running_thread do return self.running_thread do
distance = self.distance( :velocity ) distance = self.distance( :gain )
velocity = self.distance_to_velocity( distance ) gain = self.distance_to_gain( distance )
# Only set if within maximum distance. # Only set if within maximum distance.
# #
if distance > 0 && distance <= self.class.distance_max if distance > 0 && distance <= self.class.distance_max
self.log.debug "Velocity distance %0.2f (velocity %s)" % [ distance, velocity ] self.log.debug "Gain distance %0.2f (gain %s)" % [ distance, gain ]
synth.velocity = velocity synth.gain( gain )
else else
synth.velocity = 0 synth.gain( 0 )
end end
sleep self.class.sample_interval sleep self.class.sample_interval
@ -256,6 +264,15 @@ class Theremin
end 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. ### Create a new thread loop that returns when @running is false.
### ###
def running_thread( &block ) def running_thread( &block )
@ -278,11 +295,11 @@ class Theremin
end end
self.class.pitch_trigger.mode( :out ) 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.mode( :in )
self.class.pitch_echo.edge( :both ) self.class.pitch_echo.edge( :both )
self.class.velocity_echo.mode( :in ) self.class.gain_echo.mode( :in )
self.class.velocity_echo.edge( :both ) self.class.gain_echo.edge( :both )
end end
@ -295,23 +312,22 @@ class Theremin
distance_min = 0 distance_min = 0
slope = 1.0 * ( midi_end - midi_start ) / ( self.class.distance_max - distance_min ) 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 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 ) def distance_to_gain( distance )
velocity_start = 127 gain_start = 5.0
velocity_end = 1 gain_end = 0.0
distance_min = 0 distance_min = 0
slope = 1.0 * ( velocity_end - velocity_start ) / ( self.class.distance_max - distance_min ) slope = 1.0 * ( gain_end - gain_start ) / ( self.class.distance_max - distance_min )
return velocity_start + ( slope * ( distance - distance_min ) ).round return ( gain_start + ( slope * ( distance - distance_min ) ) ).round( 2 )
end end
### Returns detected distance in feet for a given +device+. ### Returns detected distance in feet for a given +device+.
### ###
def distance( device ) def distance( device )
@ -358,5 +374,12 @@ class Theremin
end end
end end
### Send text to the speech engine.
###
def speak( phrase )
self.speech_io.puts( phrase )
end
end # class Theremin end # class Theremin