mirror of
https://github.com/BlackLight/theremin.git
synced 2024-11-24 04:35:13 +01:00
First commit
This commit is contained in:
parent
d65ffe5be3
commit
42334586ce
5 changed files with 288 additions and 0 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -105,3 +105,6 @@ venv.bak/
|
||||||
|
|
||||||
# Pycharm
|
# Pycharm
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
|
# Notebook
|
||||||
|
notebook.ipynb
|
||||||
|
|
25
theremin/__init__.py
Normal file
25
theremin/__init__.py
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import pyo
|
||||||
|
|
||||||
|
from .leap import LeapMotion
|
||||||
|
from .sound import SoundProcessor
|
||||||
|
|
||||||
|
|
||||||
|
def theremin(wave='SineLoop', audio_output=None, audio_backend='portaudio', channels=2,
|
||||||
|
min_frequency=55, max_frequency=10000):
|
||||||
|
dsp = SoundProcessor(output=audio_output, backend=audio_backend, channels=channels)
|
||||||
|
dsp.start()
|
||||||
|
|
||||||
|
assert hasattr(pyo, wave)
|
||||||
|
wave = getattr(pyo, wave)
|
||||||
|
audio = wave()
|
||||||
|
channel = dsp.add_track(audio)
|
||||||
|
print('Audio processor started')
|
||||||
|
|
||||||
|
sensor = LeapMotion(dsp, track=channel, min_frequency=min_frequency, max_frequency=max_frequency)
|
||||||
|
print('Press ENTER to quit')
|
||||||
|
sys.stdin.readline()
|
||||||
|
|
||||||
|
sensor.stop()
|
||||||
|
dsp.shutdown()
|
52
theremin/__main__.py
Executable file
52
theremin/__main__.py
Executable file
|
@ -0,0 +1,52 @@
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from . import theremin
|
||||||
|
from .leap import list_leap_motions
|
||||||
|
from .sound import list_output_devices
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args(args):
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument('--list-audio-outputs', '-l', dest='list_audio_outputs', required=False,
|
||||||
|
action='store_true', help='List the available audio output devices')
|
||||||
|
parser.add_argument('--list-leap-motions', '-L', dest='list_leap_motions', required=False,
|
||||||
|
action='store_true', help='List the available Leap Motion devices')
|
||||||
|
parser.add_argument('--audio-output', '-o', dest='audio_output', required=False,
|
||||||
|
type=int, help='Select an output audio device by index (see -l)')
|
||||||
|
parser.add_argument('--audio-backend', '-a', dest='audio_backend', required=False, default='portaudio',
|
||||||
|
help='Select the audio backend (default: portaudio). Supported: ' +
|
||||||
|
'{"portaudio", "jack", "coreaudio"}')
|
||||||
|
parser.add_argument('--channels', '-c', dest='channels', required=False, type=int, default=2,
|
||||||
|
help='Number of audio channels (default: 2)')
|
||||||
|
parser.add_argument('--generator', '-g', dest='generator', required=False,
|
||||||
|
default='SineLoop', help='Wave generator to be used. See ' +
|
||||||
|
'http://ajaxsoundstudio.com/pyodoc/api/classes/generators.html. ' +
|
||||||
|
'Default: SineLoop')
|
||||||
|
parser.add_argument('--min-frequency', '-m', dest='min_frequency', required=False, type=int, default=55,
|
||||||
|
help='Minimum audio frequency (default: 55 Hz)')
|
||||||
|
parser.add_argument('--max-frequency', '-M', dest='max_frequency', required=False, type=int, default=10000,
|
||||||
|
help='Maximum audio frequency (default: 10 kHz)')
|
||||||
|
|
||||||
|
opts, args = parser.parse_known_args(args)
|
||||||
|
return opts, args
|
||||||
|
|
||||||
|
|
||||||
|
def main(args):
|
||||||
|
opts, args = parse_args(args)
|
||||||
|
|
||||||
|
if opts.list_audio_outputs:
|
||||||
|
list_output_devices()
|
||||||
|
return
|
||||||
|
|
||||||
|
if opts.list_leap_motions:
|
||||||
|
list_leap_motions()
|
||||||
|
return
|
||||||
|
|
||||||
|
theremin(wave=opts.generator, audio_backend=opts.audio_backend,
|
||||||
|
min_frequency=opts.min_frequency, max_frequency=opts.max_frequency,
|
||||||
|
audio_output=opts.audio_output, channels=opts.channels)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main(sys.argv[1:])
|
133
theremin/leap.py
Normal file
133
theremin/leap.py
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
import math
|
||||||
|
|
||||||
|
import Leap
|
||||||
|
|
||||||
|
|
||||||
|
def list_leap_motions():
|
||||||
|
controller = Leap.Controller()
|
||||||
|
has_devices = False
|
||||||
|
|
||||||
|
for device in controller.devices:
|
||||||
|
has_devices = True
|
||||||
|
print('{}'.format(str(device)))
|
||||||
|
|
||||||
|
if not has_devices:
|
||||||
|
print('No Leap Motion devices found')
|
||||||
|
|
||||||
|
|
||||||
|
class LeapMotion(Leap.Listener):
|
||||||
|
def __init__(self, dsp, track=0, amplitude=.5, min_frequency=55, max_frequency=10000):
|
||||||
|
super().__init__()
|
||||||
|
self.dsp = dsp
|
||||||
|
self.track = track
|
||||||
|
self.amplitude = amplitude
|
||||||
|
self.min_frequency = min_frequency
|
||||||
|
self.max_frequency = max_frequency
|
||||||
|
self.controller = Leap.Controller()
|
||||||
|
self.controller.add_listener(self)
|
||||||
|
|
||||||
|
def on_init(self, controller):
|
||||||
|
print('Leap initialized')
|
||||||
|
|
||||||
|
def on_connect(self, controller):
|
||||||
|
print('Leap connected')
|
||||||
|
|
||||||
|
def on_disconnect(self, controller):
|
||||||
|
print('Leap disconnected')
|
||||||
|
|
||||||
|
def on_exit(self, controller):
|
||||||
|
print('Leap exited')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_hands(frame):
|
||||||
|
(left, right) = (None, None)
|
||||||
|
|
||||||
|
if len(frame.hands) > 0:
|
||||||
|
if len(frame.hands) == 1:
|
||||||
|
left = frame.hands[0]
|
||||||
|
right = None
|
||||||
|
else:
|
||||||
|
if frame.hands[0].palm_position[0] < frame.hands[1].palm_position[0]:
|
||||||
|
left = frame.hands[0]
|
||||||
|
right = frame.hands[1]
|
||||||
|
else:
|
||||||
|
left = frame.hands[1]
|
||||||
|
right = frame.hands[0]
|
||||||
|
|
||||||
|
return left, right
|
||||||
|
|
||||||
|
def on_frame(self, controller):
|
||||||
|
frame = controller.frame()
|
||||||
|
left, right = self._get_hands(frame)
|
||||||
|
if not left and not right:
|
||||||
|
if self.dsp.is_playing(self.track):
|
||||||
|
self.dsp.stop(self.track)
|
||||||
|
return
|
||||||
|
|
||||||
|
y_left = left.palm_position[1]
|
||||||
|
frequency = self.y_to_freq(y_left)
|
||||||
|
|
||||||
|
if right:
|
||||||
|
y_right = right.palm_position[1]
|
||||||
|
self.amplitude = self.y_to_amplitude(y_right)
|
||||||
|
|
||||||
|
self.dsp.set_frequency(self.track, frequency)
|
||||||
|
self.dsp.set_volume(self.track, self.amplitude)
|
||||||
|
|
||||||
|
if not self.dsp.is_playing(self.track):
|
||||||
|
self.dsp.play(self.track)
|
||||||
|
|
||||||
|
print('Hand height: {:.8f}, Frequency: {:1f}, MIDI: {}, Amplitude: {:4f}'.format(
|
||||||
|
y_left, frequency, self.freq_to_midi_note(frequency), self.amplitude))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def midi_note_to_str(midi):
|
||||||
|
notes = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
|
||||||
|
base_c = 24
|
||||||
|
i = (midi - base_c) % len(notes)
|
||||||
|
octave = int((midi - base_c) / 12)
|
||||||
|
return '{note}{octave}'.format(note=notes[i], octave=octave)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def freq_to_midi_note(cls, freq):
|
||||||
|
return cls.midi_note_to_str(int(math.log2(freq / 440.0) * 12) + 69)
|
||||||
|
|
||||||
|
def y_to_freq(self, y):
|
||||||
|
y_min_height = 70
|
||||||
|
y_max_height = 500
|
||||||
|
min_freq = self.min_frequency
|
||||||
|
max_freq = self.max_frequency
|
||||||
|
|
||||||
|
y -= y_min_height
|
||||||
|
y_max_height -= y_min_height
|
||||||
|
# max_freq -= min_freq
|
||||||
|
|
||||||
|
frequency = min_freq + ((max_freq*y) / y_max_height)
|
||||||
|
if frequency < min_freq:
|
||||||
|
return min_freq
|
||||||
|
if frequency > max_freq:
|
||||||
|
return max_freq
|
||||||
|
|
||||||
|
return frequency
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def y_to_amplitude(y):
|
||||||
|
y_min_height = 70
|
||||||
|
y_max_height = 500
|
||||||
|
min_amplitude = 0
|
||||||
|
max_amplitude = 1.5
|
||||||
|
|
||||||
|
y -= y_min_height
|
||||||
|
y_max_height -= y_min_height
|
||||||
|
max_amplitude -= min_amplitude
|
||||||
|
|
||||||
|
amplitude = min_amplitude + ((max_amplitude*y) / y_max_height)
|
||||||
|
if amplitude < min_amplitude:
|
||||||
|
return min_amplitude
|
||||||
|
if amplitude > max_amplitude:
|
||||||
|
return max_amplitude
|
||||||
|
|
||||||
|
return amplitude
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.controller.remove_listener(self)
|
75
theremin/sound.py
Normal file
75
theremin/sound.py
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
from pyo import *
|
||||||
|
|
||||||
|
|
||||||
|
def list_output_devices():
|
||||||
|
pa_list_devices()
|
||||||
|
|
||||||
|
|
||||||
|
class Track:
|
||||||
|
def __init__(self, audio):
|
||||||
|
self.audio = audio
|
||||||
|
self.playing = False
|
||||||
|
|
||||||
|
def play(self):
|
||||||
|
self.audio.out()
|
||||||
|
self.playing = True
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.audio.stop()
|
||||||
|
self.playing = False
|
||||||
|
|
||||||
|
def set_volume(self, volume):
|
||||||
|
self.audio.setMul(volume)
|
||||||
|
|
||||||
|
def set_frequency(self, frequency):
|
||||||
|
self.audio.setFreq(frequency)
|
||||||
|
|
||||||
|
|
||||||
|
class SoundProcessor:
|
||||||
|
def __init__(self, backend='portaudio', output=None, channels=2):
|
||||||
|
self.backend = backend
|
||||||
|
self.server = Server(audio=self.backend, jackname='theremin', winhost='theremin', nchnls=channels)
|
||||||
|
self.audio_output = output or pa_get_default_output()
|
||||||
|
self.server.setOutputDevice(self.audio_output)
|
||||||
|
self.tracks = []
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
self.server.boot()
|
||||||
|
self.server.start()
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
self.stop()
|
||||||
|
self.server.stop()
|
||||||
|
self.server.shutdown()
|
||||||
|
|
||||||
|
def add_track(self, audio):
|
||||||
|
self.tracks.append(Track(audio))
|
||||||
|
return len(self.tracks) - 1
|
||||||
|
|
||||||
|
def remove_track(self, i):
|
||||||
|
assert 0 <= i < len(self.tracks)
|
||||||
|
return self.tracks.pop(i)
|
||||||
|
|
||||||
|
def play(self, i):
|
||||||
|
assert 0 <= i < len(self.tracks)
|
||||||
|
self.tracks[i].play()
|
||||||
|
|
||||||
|
def stop(self, i=None):
|
||||||
|
if i is not None:
|
||||||
|
assert 0 <= i < len(self.tracks)
|
||||||
|
self.tracks[i].stop()
|
||||||
|
else:
|
||||||
|
for track in self.tracks:
|
||||||
|
track.stop()
|
||||||
|
|
||||||
|
def is_playing(self, i):
|
||||||
|
assert 0 <= i < len(self.tracks)
|
||||||
|
return self.tracks[i].playing
|
||||||
|
|
||||||
|
def set_volume(self, i, volume):
|
||||||
|
assert 0 <= i < len(self.tracks)
|
||||||
|
self.tracks[i].set_volume(volume)
|
||||||
|
|
||||||
|
def set_frequency(self, i, frequency):
|
||||||
|
assert 0 <= i < len(self.tracks)
|
||||||
|
self.tracks[i].set_frequency(frequency)
|
Loading…
Reference in a new issue