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:
parent
5a4fcbae87
commit
7bc47cfc4d
4 changed files with 58 additions and 35 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue