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:
# 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

View file

@ -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

View file

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

View file

@ -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 ]
@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
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