Initial commit.
Stuff working well with a single 3-pin range finder. FossilOrigin-Name: 483d8caaef350827c026d7c234b026e9e720d26a7e27e54b0436f60b099a0ab9
This commit is contained in:
parent
af047120c4
commit
07b2d81523
9 changed files with 674 additions and 0 deletions
20
README.md
Normal file
20
README.md
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
|
||||||
|
# RPi Theremin
|
||||||
|
|
||||||
|
This is a quick-n-dirty Theremin daemon for use on the RaspberryPi.
|
||||||
|
|
||||||
|
It is designed to be used with a couple of these distance sensors:
|
||||||
|
|
||||||
|
https://www.adafruit.com/product/4007
|
||||||
|
|
||||||
|
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
|
||||||
|
# gem install -Ng
|
||||||
|
# ln -s /path/to/here/theremin /etc/service
|
||||||
|
# ln -s /path/to/here/fluidsynth /etc/service
|
||||||
|
|
||||||
|
|
||||||
12
fluidsynth/run
Executable file
12
fluidsynth/run
Executable file
|
|
@ -0,0 +1,12 @@
|
||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# Run fluidsynth as a server, with some more dedicated CPU "oomph".
|
||||||
|
#
|
||||||
|
|
||||||
|
exec 2>&1
|
||||||
|
exec fluidsynth -v -n -i -s \
|
||||||
|
-a alsa -c2 -z1024 \
|
||||||
|
-o synth.cpu-cores=4 \
|
||||||
|
-o audio.realtime-prio=95 \
|
||||||
|
-o audio.alsa.device=hw:0
|
||||||
|
|
||||||
4
gem.deps.rb
Normal file
4
gem.deps.rb
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
source 'https://rubygems.org/'
|
||||||
|
|
||||||
|
gem 'configurability', '~> 4.2'
|
||||||
|
|
||||||
BIN
theremin/Theremin.sf2
Normal file
BIN
theremin/Theremin.sf2
Normal file
Binary file not shown.
18
theremin/config.yml
Normal file
18
theremin/config.yml
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# -*- ruby -*-
|
||||||
|
# vim: set et sta sw=2 :
|
||||||
|
|
||||||
|
---
|
||||||
|
logging:
|
||||||
|
__default__: info (color)
|
||||||
|
theremin: debug (color)
|
||||||
|
|
||||||
|
theremin:
|
||||||
|
pitch_trigger: 16
|
||||||
|
font: /usr/share/sounds/sf2/FluidR3_GM.sf2
|
||||||
|
glide: 1
|
||||||
|
slew: 0
|
||||||
|
sample_interval: 0.1
|
||||||
|
reverb: false
|
||||||
|
chorus: false
|
||||||
|
instrument_index: 29
|
||||||
|
|
||||||
187
theremin/lib/fluidsynth.rb
Normal file
187
theremin/lib/fluidsynth.rb
Normal file
|
|
@ -0,0 +1,187 @@
|
||||||
|
# -*- ruby -*-
|
||||||
|
# vim: set noet sta sw=4 ts=4 :
|
||||||
|
|
||||||
|
require 'socket'
|
||||||
|
require 'configurability'
|
||||||
|
require 'loggability'
|
||||||
|
|
||||||
|
|
||||||
|
# Communicate with a running FluidSynth daemon via it's TCP socket API.
|
||||||
|
#
|
||||||
|
class FluidSynth
|
||||||
|
extend Configurability,
|
||||||
|
Loggability
|
||||||
|
|
||||||
|
# The default host for the fluidsynth server.
|
||||||
|
DEFAULT_HOST = 'localhost'
|
||||||
|
|
||||||
|
# The default port for the fluidsynth server.
|
||||||
|
DEFAULT_PORT = 9800
|
||||||
|
|
||||||
|
|
||||||
|
# Loggability API
|
||||||
|
log_as :fluidsynth
|
||||||
|
|
||||||
|
# Configuration API
|
||||||
|
#
|
||||||
|
configurability( :fluidsynth ) do
|
||||||
|
|
||||||
|
##
|
||||||
|
# How many simultaneous notes can play. Keep low for better CPU.
|
||||||
|
setting :polyphony, default: 6
|
||||||
|
|
||||||
|
##
|
||||||
|
# Reverb amount.
|
||||||
|
setting :reverb_size, default: 0.9
|
||||||
|
|
||||||
|
##
|
||||||
|
# Reverb gain level.
|
||||||
|
setting :reverb_gain, default: 0.7
|
||||||
|
|
||||||
|
##
|
||||||
|
# Chorus depth.
|
||||||
|
setting :chorus_depth, default: 64.0
|
||||||
|
|
||||||
|
##
|
||||||
|
# Chorus gain level.
|
||||||
|
setting :chorus_gain, default: 1.5
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def initialize( host=DEFAULT_HOST, port=DEFAULT_PORT )
|
||||||
|
@host = host
|
||||||
|
@port = port
|
||||||
|
@lastnote = nil
|
||||||
|
@velocity = 80
|
||||||
|
end
|
||||||
|
|
||||||
|
# The TCP socket to fluidsynth.
|
||||||
|
attr_reader :socket
|
||||||
|
|
||||||
|
# The previous note played, to enable glides.
|
||||||
|
attr_reader :lastnote
|
||||||
|
|
||||||
|
# The current velocity to play new notes at.
|
||||||
|
attr_accessor :velocity
|
||||||
|
|
||||||
|
|
||||||
|
### Open the TCP socket to the synth and perform initial setups.
|
||||||
|
###
|
||||||
|
def connect
|
||||||
|
@socket = TCPSocket.new( @host, @port )
|
||||||
|
|
||||||
|
self.setup_effects
|
||||||
|
self.polyphony( self.class.polyphony )
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### Setup polyphony amount.
|
||||||
|
###
|
||||||
|
def polyphony( val )
|
||||||
|
self.send "synth.polyphony #{val}"
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### Stop all notes and reset effects.
|
||||||
|
###
|
||||||
|
def panic
|
||||||
|
self.send "reset"
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### Alter the default volume.
|
||||||
|
###
|
||||||
|
def gain( level )
|
||||||
|
self.send "gain #{level}"
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### Turn glide (portamento) on or off. Val is an integer that
|
||||||
|
### represents 128ms. 4 == 512ms.
|
||||||
|
###
|
||||||
|
def glide( val )
|
||||||
|
if val.zero?
|
||||||
|
self.send "cc 0 68 0"
|
||||||
|
self.send "cc 0 65 0"
|
||||||
|
self.send "cc 0 5 0"
|
||||||
|
else
|
||||||
|
self.send "setlegatomode 0 1"
|
||||||
|
self.send "cc 0 68 127" # legato
|
||||||
|
self.send "cc 0 65 127" # portamento
|
||||||
|
self.send "cc 0 5 #{val}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### Play a midi +note+ at +velocity+, then silence the previous note.
|
||||||
|
### (This allows portamento to function if enabled.)
|
||||||
|
###
|
||||||
|
def playnote( note, velocity=self.velocity )
|
||||||
|
return if @lastnote == note
|
||||||
|
self.send "noteon 0 #{note} #{velocity}"
|
||||||
|
self.stopnote
|
||||||
|
@lastnote = note
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### Silences the previous note played.
|
||||||
|
###
|
||||||
|
def stopnote
|
||||||
|
return unless @lastnote
|
||||||
|
self.send "noteoff 0 #{@lastnote}"
|
||||||
|
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+.
|
||||||
|
###
|
||||||
|
def load( path )
|
||||||
|
self.send "load #{path}"
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### Enable/disable reverb.
|
||||||
|
###
|
||||||
|
def reverb( toggle=true )
|
||||||
|
toggle = toggle ? 1 : 0
|
||||||
|
self.send "set synth.reverb.active #{toggle}"
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### Enable/disable chorus.
|
||||||
|
###
|
||||||
|
def chorus( toggle=true )
|
||||||
|
toggle = toggle ? 1 : 0
|
||||||
|
self.send "set synth.chorus.active #{toggle}"
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
#########
|
||||||
|
protected
|
||||||
|
#########
|
||||||
|
|
||||||
|
### Initial effect setup.
|
||||||
|
###
|
||||||
|
def setup_effects
|
||||||
|
self.send "set synth.reverb.room-size 0.9"
|
||||||
|
self.send "set synth.reverb.level 0.7"
|
||||||
|
self.send "set synth.chorus.level 1.5"
|
||||||
|
self.send "set synth.chorus.depth 64.0"
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### Issue a command to the synth, after possible logging.
|
||||||
|
###
|
||||||
|
def send( cmd )
|
||||||
|
self.log.debug( cmd )
|
||||||
|
self.socket.puts( cmd )
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
121
theremin/lib/gpio.rb
Executable file
121
theremin/lib/gpio.rb
Executable file
|
|
@ -0,0 +1,121 @@
|
||||||
|
# -*- ruby -*-
|
||||||
|
# vim: set noet sta sw=4 ts=4 :
|
||||||
|
|
||||||
|
require 'socket'
|
||||||
|
require 'configurability'
|
||||||
|
require 'loggability'
|
||||||
|
require 'pathname'
|
||||||
|
|
||||||
|
|
||||||
|
# A representation of a GPIO pin.
|
||||||
|
# Manages basic reads/writes (HIGH/LOW).
|
||||||
|
#
|
||||||
|
class GPIO
|
||||||
|
extend Configurability,
|
||||||
|
Loggability
|
||||||
|
|
||||||
|
# Default read timeout.
|
||||||
|
TIMEOUT = 0.5
|
||||||
|
|
||||||
|
# The sysfs path.
|
||||||
|
BASE = Pathname( "/sys/class/gpio" )
|
||||||
|
|
||||||
|
|
||||||
|
# Loggability API
|
||||||
|
log_as :gpio
|
||||||
|
|
||||||
|
|
||||||
|
### Instance a new GPIO at +pin+. It will be automatically exported.
|
||||||
|
###
|
||||||
|
def initialize( pin )
|
||||||
|
@pin = pin
|
||||||
|
@paths = {
|
||||||
|
export: BASE + "export",
|
||||||
|
unexport: BASE + "unexport",
|
||||||
|
edge: BASE + "gpio#{pin}" + "edge",
|
||||||
|
direction: BASE + "gpio#{pin}" + "direction",
|
||||||
|
value: BASE + "gpio#{pin}" + "value"
|
||||||
|
}
|
||||||
|
|
||||||
|
self.export rescue Errno::EBUSY nil
|
||||||
|
end
|
||||||
|
|
||||||
|
# The GPIO PIN number.
|
||||||
|
attr_reader :pin
|
||||||
|
|
||||||
|
|
||||||
|
### Mark this pin as either input or output.
|
||||||
|
###
|
||||||
|
def mode( dir )
|
||||||
|
case dir.to_sym
|
||||||
|
when :in, :out
|
||||||
|
self.set( :direction ) { dir.to_s }
|
||||||
|
else
|
||||||
|
raise "Mode must be :in or :out."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### Hint at voltage measurement capture.
|
||||||
|
###
|
||||||
|
def edge( mode )
|
||||||
|
case mode.to_sym
|
||||||
|
when :none, :rising, :falling, :both
|
||||||
|
self.set( :edge ) { mode.to_s }
|
||||||
|
else
|
||||||
|
raise "Edge must be one of: :none, :rising, :falling, or :both"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### Tell the operating system to make this gpio available to sysfs.
|
||||||
|
###
|
||||||
|
def export
|
||||||
|
self.set( :export ) { self.pin }
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### Remove this gpio from sysfs.
|
||||||
|
###
|
||||||
|
def unexport
|
||||||
|
self.set( :unexport ) { self.pin }
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### Write a single +val+ to the GPIO.
|
||||||
|
###
|
||||||
|
def write( val )
|
||||||
|
self.set( :value ) { val }
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### Read a single value from the GPIO, with an optional timeout.
|
||||||
|
###
|
||||||
|
def read( timeout=TIMEOUT )
|
||||||
|
io = @paths[ :value ].open
|
||||||
|
|
||||||
|
ready = IO.select( [io], nil, nil, timeout )
|
||||||
|
return unless ready # timeout
|
||||||
|
|
||||||
|
rv = io.read( 1 ).to_i
|
||||||
|
self.log.debug "Read %p from GPIO %d" % [ rv, self.pin ]
|
||||||
|
return rv
|
||||||
|
ensure
|
||||||
|
io.close
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
#########
|
||||||
|
protected
|
||||||
|
#########
|
||||||
|
|
||||||
|
### Generic write helper.
|
||||||
|
###
|
||||||
|
def set( path, &block )
|
||||||
|
@paths[ path ].open( 'w' ) do |f|
|
||||||
|
self.log.debug "Writing %p to %p of GPIO %d" % [ block.yield, path, self.pin ]
|
||||||
|
f.puts( block.yield )
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
293
theremin/lib/theremin.rb
Executable file
293
theremin/lib/theremin.rb
Executable file
|
|
@ -0,0 +1,293 @@
|
||||||
|
# -*- ruby -*-
|
||||||
|
# vim: set noet sta sw=4 ts=4 :
|
||||||
|
|
||||||
|
require 'forwardable'
|
||||||
|
require 'configurability'
|
||||||
|
require 'loggability'
|
||||||
|
require 'gpio'
|
||||||
|
require 'fluidsynth'
|
||||||
|
|
||||||
|
|
||||||
|
# A class that coordinates GPIO events and translates
|
||||||
|
# them into commands for Fluidsynth.
|
||||||
|
#
|
||||||
|
class Theremin
|
||||||
|
extend Configurability,
|
||||||
|
Loggability,
|
||||||
|
Forwardable
|
||||||
|
|
||||||
|
# The speed of sound, in centmeters/sec.
|
||||||
|
SONICSPEED = 34300
|
||||||
|
|
||||||
|
# Loggability API
|
||||||
|
log_as :theremin
|
||||||
|
|
||||||
|
# Configuration API
|
||||||
|
#
|
||||||
|
configurability( :theremin ) do
|
||||||
|
|
||||||
|
##
|
||||||
|
# The host that fluidsynth lives at.
|
||||||
|
setting :host, default: 'localhost'
|
||||||
|
|
||||||
|
##
|
||||||
|
# The port that fluidsynth lives at.
|
||||||
|
setting :port, default: 9800
|
||||||
|
|
||||||
|
##
|
||||||
|
# The GPIO pin the pitch trigger is on.
|
||||||
|
setting :pitch_trigger do |val|
|
||||||
|
GPIO.new( val ) if val
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# The GPIO pin the pitch echo measurement is on.
|
||||||
|
setting :pitch_echo do |val|
|
||||||
|
GPIO.new( val ) if val
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# The GPIO pin the velocity trigger is on.
|
||||||
|
setting :velocity_trigger do |val|
|
||||||
|
GPIO.new( val ) if val
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# The GPIO pin the velocity echo measurement is on.
|
||||||
|
setting :velocity_echo do |val|
|
||||||
|
GPIO.new( val ) if val
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# The soundfont to use for the synth. Will look in the
|
||||||
|
# current working directory by default, but absolute paths
|
||||||
|
# can be used for system sounds.
|
||||||
|
setting :font, default: 'Theremin.sf2' do |val|
|
||||||
|
val = Pathname( val )
|
||||||
|
val = Pathname.pwd + val unless val.absolute?
|
||||||
|
val
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# If a note is "stuck", turn if off after this many seconds.
|
||||||
|
setting :cancel_after, default: 5
|
||||||
|
|
||||||
|
##
|
||||||
|
# A reasonable distance in feet for the theremin to map notes.
|
||||||
|
setting :distance_max, default: 4
|
||||||
|
|
||||||
|
##
|
||||||
|
# Initial synth volume.
|
||||||
|
setting :gain, default: 1.0
|
||||||
|
|
||||||
|
##
|
||||||
|
# Enable reverb?
|
||||||
|
setting :reverb, default: true
|
||||||
|
|
||||||
|
##
|
||||||
|
# Enable chorus?
|
||||||
|
setting :chorus, default: true
|
||||||
|
|
||||||
|
##
|
||||||
|
# What instrument to use from the loaded SF2.
|
||||||
|
setting :instrument_index, default: 1
|
||||||
|
|
||||||
|
##
|
||||||
|
# Portamento. Value in milliseconds to glide between notes to
|
||||||
|
# emulate analog theremin. 0 disables completely. Values
|
||||||
|
# are in 128ms increments.
|
||||||
|
setting :glide, default: 768 do |val|
|
||||||
|
val = 128 if val < 128 && ! val.zero?
|
||||||
|
val = ( val.to_f / 128 ).round
|
||||||
|
val
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# How frequently should the range finders measure distance?
|
||||||
|
# Value in seconds.
|
||||||
|
setting :sample_interval, default: 0.3
|
||||||
|
|
||||||
|
##
|
||||||
|
# Only change the note if the range finder value moves +/- this much
|
||||||
|
# between samples. This helps hold pitch, and avoids "warbles".
|
||||||
|
setting :slew, default: 5
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### Instance a new Theremin daemon.
|
||||||
|
###
|
||||||
|
def initialize
|
||||||
|
# TODO: @pins = %i[ pitch_trigger pitch_echo velocity_trigger velocity_echo ]
|
||||||
|
@pins = %i[ pitch_trigger ]
|
||||||
|
|
||||||
|
self.setup_gpio
|
||||||
|
|
||||||
|
@timeout = 0
|
||||||
|
@synth = FluidSynth.new( self.class.host, self.class.port )
|
||||||
|
@running = false
|
||||||
|
@threads = []
|
||||||
|
|
||||||
|
rescue => err
|
||||||
|
self.log.warn( err.message )
|
||||||
|
self.stop rescue nil
|
||||||
|
end
|
||||||
|
|
||||||
|
# Delegate various methods directly to the underlying synth instance.
|
||||||
|
def_delegators :@synth, :gain, :glide, :load, :reverb, :chorus, :instrument
|
||||||
|
|
||||||
|
# The names of the GPIO pins.
|
||||||
|
attr_reader :pins
|
||||||
|
|
||||||
|
# The Fluidsynth instance.
|
||||||
|
attr_reader :synth
|
||||||
|
|
||||||
|
# Is this Theremin currently running?
|
||||||
|
attr_reader :running
|
||||||
|
|
||||||
|
|
||||||
|
### Start polling the GPIO for measurements.
|
||||||
|
###
|
||||||
|
def start
|
||||||
|
@running = true
|
||||||
|
self.synth.connect
|
||||||
|
|
||||||
|
self.gain( self.class.gain )
|
||||||
|
self.glide( self.class.glide )
|
||||||
|
self.load( self.class.font.to_s )
|
||||||
|
self.reverb( self.class.reverb )
|
||||||
|
self.chorus( self.class.chorus )
|
||||||
|
self.instrument( self.class.instrument_index )
|
||||||
|
|
||||||
|
@threads << self.note_thread
|
||||||
|
self.log.warn "Waiting for interaction, or ctrl-c to exit."
|
||||||
|
@threads.map( &:join )
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### Cleanly shutdown GPIO and synth.
|
||||||
|
###
|
||||||
|
def stop
|
||||||
|
@running = false
|
||||||
|
self.pins.each {|pin| self.class.send( pin ).unexport rescue nil }
|
||||||
|
self.synth.socket.close
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
#########
|
||||||
|
protected
|
||||||
|
#########
|
||||||
|
|
||||||
|
### Measure pitch distance, and manage notes sent to the synth.
|
||||||
|
###
|
||||||
|
def note_thread
|
||||||
|
self.log.info "Starting note/pitch measurements (every %s/sec) within %d feet." % [
|
||||||
|
self.class.sample_interval,
|
||||||
|
self.class.distance_max
|
||||||
|
]
|
||||||
|
|
||||||
|
return self.running_thread do
|
||||||
|
distance = self.distance
|
||||||
|
note = self.distance_to_note( distance )
|
||||||
|
|
||||||
|
# Only play if within maximum distance.
|
||||||
|
#
|
||||||
|
if distance <= self.class.distance_max
|
||||||
|
self.log.debug "Pitch distance %0.2f (note %s)" % [ distance, note ]
|
||||||
|
|
||||||
|
# Hold the note if it is unchanged within slew amount.
|
||||||
|
#
|
||||||
|
if synth.lastnote
|
||||||
|
if note <= synth.lastnote - self.class.slew || note >= synth.lastnote + self.class.slew
|
||||||
|
synth.playnote( note )
|
||||||
|
end
|
||||||
|
else
|
||||||
|
synth.playnote( note )
|
||||||
|
end
|
||||||
|
|
||||||
|
# Cancel note if timeout is hit.
|
||||||
|
#
|
||||||
|
elsif @timeout >= self.class.cancel_after
|
||||||
|
@timeout = 0
|
||||||
|
synth.stopnote
|
||||||
|
end
|
||||||
|
|
||||||
|
sleep self.class.sample_interval
|
||||||
|
@timeout += self.class.sample_interval
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### Create a new thread loop that returns when @running is false.
|
||||||
|
###
|
||||||
|
def running_thread( &block )
|
||||||
|
return Thread.new do
|
||||||
|
while @running
|
||||||
|
yield
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### Validate GPIO config, and mark edge behavior for the echo measurement pins.
|
||||||
|
###
|
||||||
|
def setup_gpio
|
||||||
|
pincheck = self.pins.map{|p| self.class.send(p) }
|
||||||
|
|
||||||
|
if pincheck.any?( nil )
|
||||||
|
self.stop rescue nil
|
||||||
|
raise "GPIO pins are required configuration options: %p" % [ self.pins ]
|
||||||
|
end
|
||||||
|
|
||||||
|
self.class.pitch_trigger.edge( :both )
|
||||||
|
# self.class.pitch_echo.edge( :both )
|
||||||
|
# TODO: self.class.velocity_echo.edge( :both )
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### For a given +distance+ in feet, convert into a valid MIDI note
|
||||||
|
### within the range.
|
||||||
|
###
|
||||||
|
def distance_to_note( distance )
|
||||||
|
midi_start = 107 # 127, but the theramin soundfont maxes out at 107.
|
||||||
|
midi_end = 0
|
||||||
|
distance_min = 0
|
||||||
|
|
||||||
|
slope = 1.0 * ( midi_end - midi_start ) / ( self.class.distance_max - distance_min )
|
||||||
|
return midi_start + ( slope * ( distance - distance_min ) ).round
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### TODO: make generic for a trigger/echo pin pair
|
||||||
|
### Returns detected distance in feet.
|
||||||
|
###
|
||||||
|
def distance
|
||||||
|
self.class.pitch_trigger.mode( :out )
|
||||||
|
self.class.pitch_trigger.write( 1 )
|
||||||
|
sleep 0.00001
|
||||||
|
self.class.pitch_trigger.write( 0 )
|
||||||
|
self.class.pitch_trigger.mode( :in )
|
||||||
|
|
||||||
|
duration = Time.now
|
||||||
|
while self.class.pitch_trigger.read == 0
|
||||||
|
starttime = Time.now
|
||||||
|
break if starttime - duration > GPIO::TIMEOUT
|
||||||
|
end
|
||||||
|
while self.class.pitch_trigger.read == 1
|
||||||
|
stoptime = Time.now
|
||||||
|
break if stoptime - duration > GPIO::TIMEOUT
|
||||||
|
end
|
||||||
|
|
||||||
|
elapsed = stoptime - starttime
|
||||||
|
distance = ( elapsed * SONICSPEED ) / 2
|
||||||
|
distance = 0 if distance < 0
|
||||||
|
|
||||||
|
distance = distance / 30.48
|
||||||
|
|
||||||
|
return distance
|
||||||
|
|
||||||
|
rescue
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
19
theremin/run
Executable file
19
theremin/run
Executable file
|
|
@ -0,0 +1,19 @@
|
||||||
|
#!/usr/bin/env ruby
|
||||||
|
# vim: set noet sta sw=4 ts=4 :
|
||||||
|
|
||||||
|
$LOAD_PATH << './lib'
|
||||||
|
require 'pathname'
|
||||||
|
require 'configurability'
|
||||||
|
require 'theremin'
|
||||||
|
|
||||||
|
Configurability::Config.load( 'config.yml' ).install
|
||||||
|
|
||||||
|
theremin = Theremin.new
|
||||||
|
|
||||||
|
Signal.trap( "INT" ) do
|
||||||
|
puts "\n\nShutting down..."
|
||||||
|
theremin.stop if theremin.running
|
||||||
|
end
|
||||||
|
|
||||||
|
theremin.start
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue