mirror of
https://github.com/BlackLight/theremin.git
synced 2024-11-24 04:35:13 +01:00
Added multiple features and extended README
This commit is contained in:
parent
42334586ce
commit
770359a9c0
9 changed files with 321 additions and 104 deletions
87
README.md
87
README.md
|
@ -1,2 +1,87 @@
|
||||||
# theremin
|
# theremin
|
||||||
Theremin synth emulator in Python
|
|
||||||
|
This is a [theremin](https://en.wikipedia.org/wiki/Theremin) synth emulator written in Python that
|
||||||
|
leverages a [Leap Motion](https://www.leapmotion.com/) device as a controller for pitch, timber and volume.
|
||||||
|
|
||||||
|
Make music waving your hands: now you can.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
You'll need a [Leap Motion](https://www.leapmotion.com/) device and the
|
||||||
|
[Leap Motion SDK](https://developer-archive.leapmotion.com/documentation/python/index.html)
|
||||||
|
installed. You'll need in particular the `Leap.py` script to be somewhere in your Python libpath
|
||||||
|
as well as the `leapd` executable from the SDK.
|
||||||
|
|
||||||
|
You'll ealso need the [PYO](https://github.com/belangeo/pyo) DSP module installed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install pyo
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, clone and install this repo:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/BlackLight/theremin
|
||||||
|
cd theremin
|
||||||
|
[sudo] python setup.py install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Plug your Leap Motion, start the `leapd` daemon and make sure that your device is detected:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
[sudo] leapd &
|
||||||
|
```
|
||||||
|
|
||||||
|
Start the theremin:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
theremin
|
||||||
|
```
|
||||||
|
|
||||||
|
Move your hands and enjoy the fun!
|
||||||
|
|
||||||
|
You can set the pitch of the sound by moving your right hand up and down, while the height of
|
||||||
|
the left hand will set the volume. Use the `--left-handed` option to invert the order.
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
```
|
||||||
|
usage: theremin [-h] [--list-audio-outputs] [--list-leap-motions]
|
||||||
|
[--audio-output AUDIO_OUTPUT] [--audio-backend AUDIO_BACKEND]
|
||||||
|
[--channels CHANNELS] [--discrete] [--left-handed]
|
||||||
|
[--generator GENERATOR] [--min-frequency MIN_FREQUENCY]
|
||||||
|
[--max-frequency MAX_FREQUENCY] [--min-note MIN_NOTE]
|
||||||
|
[--max-note MAX_NOTE]
|
||||||
|
|
||||||
|
optional arguments:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
--list-audio-outputs, -l
|
||||||
|
List the available audio output devices
|
||||||
|
--list-leap-motions, -L
|
||||||
|
List the available Leap Motion devices
|
||||||
|
--audio-output AUDIO_OUTPUT, -o AUDIO_OUTPUT
|
||||||
|
Select an output audio device by index (see -l)
|
||||||
|
--audio-backend AUDIO_BACKEND, -b AUDIO_BACKEND
|
||||||
|
Select the audio backend (default: portaudio).
|
||||||
|
Supported: {"portaudio", "jack", "coreaudio"}
|
||||||
|
--channels CHANNELS, -c CHANNELS
|
||||||
|
Number of audio channels (default: 2)
|
||||||
|
--discrete, -d If set then discrete notes will be generated instead
|
||||||
|
of samples over a continuous frequency space (default:
|
||||||
|
false)
|
||||||
|
--left-handed If set then the pitch control will be on the left hand
|
||||||
|
and the volume control on theright hand. Otherwise,
|
||||||
|
the controls are inverted (default: false)
|
||||||
|
--generator GENERATOR, -g GENERATOR
|
||||||
|
Wave generator to be used. See http://ajaxsoundstudio.
|
||||||
|
com/pyodoc/api/classes/generators.html. Default:
|
||||||
|
SineLoop
|
||||||
|
--min-frequency MIN_FREQUENCY
|
||||||
|
Minimum audio frequency (default: 55 Hz)
|
||||||
|
--max-frequency MAX_FREQUENCY
|
||||||
|
Maximum audio frequency (default: 10 kHz)
|
||||||
|
--min-note MIN_NOTE Minimum MIDI note, as a string (e.g. A4)
|
||||||
|
--max-note MAX_NOTE Maximum MIDI note, as a string (e.g. A4)
|
||||||
|
```
|
||||||
|
|
4
main.py
Normal file
4
main.py
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
from theremin import main
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
48
setup.py
Executable file
48
setup.py
Executable file
|
@ -0,0 +1,48 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
|
|
||||||
|
def path(fname=''):
|
||||||
|
return os.path.abspath(os.path.join(os.path.dirname(__file__), fname))
|
||||||
|
|
||||||
|
|
||||||
|
def readfile(fname):
|
||||||
|
with open(path(fname)) as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name="theremin",
|
||||||
|
version="0.1",
|
||||||
|
author="Fabio Manganiello",
|
||||||
|
author_email="info@fabiomanganiello.com",
|
||||||
|
description="A theremin synth emulator controllable through a Leap Motion",
|
||||||
|
license="MIT",
|
||||||
|
python_requires='>= 3.5',
|
||||||
|
keywords="music synth theremin pyo leap_motion",
|
||||||
|
url="https://github.com/BlackLight/theremin",
|
||||||
|
packages=find_packages(),
|
||||||
|
include_package_data=True,
|
||||||
|
entry_points={
|
||||||
|
'console_scripts': [
|
||||||
|
'theremin=theremin:main',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
long_description=readfile('README.md'),
|
||||||
|
long_description_content_type='text/markdown',
|
||||||
|
classifiers=[
|
||||||
|
"Topic :: Multimedia :: Sound/Audio :: Sound Synthesis",
|
||||||
|
"License :: OSI Approved :: MIT License",
|
||||||
|
"Development Status :: 3 - Alpha",
|
||||||
|
],
|
||||||
|
install_requires=[
|
||||||
|
'pyo',
|
||||||
|
# Also requires Leap.py from the Leap Motion SDK installation
|
||||||
|
# see https://developer-archive.leapmotion.com/documentation/python/index.html
|
||||||
|
],
|
||||||
|
extras_require={
|
||||||
|
},
|
||||||
|
)
|
78
theremin/__init__.py
Normal file → Executable file
78
theremin/__init__.py
Normal file → Executable file
|
@ -1,25 +1,67 @@
|
||||||
|
import argparse
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import pyo
|
from .leap import list_leap_motions
|
||||||
|
from .sound import list_output_devices
|
||||||
from .leap import LeapMotion
|
from .sound.utils import midi_str_to_midi, midi_to_freq
|
||||||
from .sound import SoundProcessor
|
from .theremin import theremin
|
||||||
|
|
||||||
|
|
||||||
def theremin(wave='SineLoop', audio_output=None, audio_backend='portaudio', channels=2,
|
def parse_args(args):
|
||||||
min_frequency=55, max_frequency=10000):
|
parser = argparse.ArgumentParser()
|
||||||
dsp = SoundProcessor(output=audio_output, backend=audio_backend, channels=channels)
|
parser.add_argument('--list-audio-outputs', '-l', dest='list_audio_outputs', required=False,
|
||||||
dsp.start()
|
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', '-b', 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('--discrete', '-d', dest='discrete', required=False, action='store_true',
|
||||||
|
help='If set then discrete notes will be generated instead of samples over a continuous ' +
|
||||||
|
'frequency space (default: false)')
|
||||||
|
parser.add_argument('--left-handed', dest='left_handed', required=False, action='store_true',
|
||||||
|
help='If set then the pitch control will be on the left hand and the volume control on the' +
|
||||||
|
'right hand. Otherwise, the controls are inverted (default: false)')
|
||||||
|
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', dest='min_frequency', required=False, type=int, default=55,
|
||||||
|
help='Minimum audio frequency (default: 55 Hz)')
|
||||||
|
parser.add_argument('--max-frequency', dest='max_frequency', required=False, type=int, default=10000,
|
||||||
|
help='Maximum audio frequency (default: 10 kHz)')
|
||||||
|
parser.add_argument('--min-note', dest='min_note', required=False, type=str, default=None,
|
||||||
|
help='Minimum MIDI note, as a string (e.g. A4)')
|
||||||
|
parser.add_argument('--max-note', dest='max_note', required=False, type=str, default=None,
|
||||||
|
help='Maximum MIDI note, as a string (e.g. A4)')
|
||||||
|
|
||||||
assert hasattr(pyo, wave)
|
opts, args = parser.parse_known_args(args)
|
||||||
wave = getattr(pyo, wave)
|
return opts, args
|
||||||
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()
|
def main(args=None):
|
||||||
dsp.shutdown()
|
if not args:
|
||||||
|
args = sys.argv[1:]
|
||||||
|
|
||||||
|
opts, args = parse_args(args)
|
||||||
|
|
||||||
|
if opts.list_audio_outputs:
|
||||||
|
list_output_devices()
|
||||||
|
return
|
||||||
|
|
||||||
|
if opts.list_leap_motions:
|
||||||
|
list_leap_motions()
|
||||||
|
return
|
||||||
|
|
||||||
|
if opts.min_note:
|
||||||
|
opts.min_frequency = midi_to_freq(midi_str_to_midi(opts.min_note))
|
||||||
|
if opts.max_note:
|
||||||
|
opts.max_frequency = midi_to_freq(midi_str_to_midi(opts.max_note))
|
||||||
|
|
||||||
|
theremin(wave=opts.generator, audio_backend=opts.audio_backend, discrete=opts.discrete,
|
||||||
|
min_frequency=opts.min_frequency, max_frequency=opts.max_frequency, left_handed=opts.left_handed,
|
||||||
|
audio_output=opts.audio_output, channels=opts.channels)
|
||||||
|
|
52
theremin/__main__.py
Executable file → Normal file
52
theremin/__main__.py
Executable file → Normal file
|
@ -1,52 +1,4 @@
|
||||||
import argparse
|
from . import main
|
||||||
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__":
|
if __name__ == "__main__":
|
||||||
main(sys.argv[1:])
|
main()
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import math
|
|
||||||
|
|
||||||
import Leap
|
import Leap
|
||||||
|
|
||||||
|
from .sound.utils import freq_to_midi_str, freq_to_midi, midi_to_freq
|
||||||
|
|
||||||
|
|
||||||
def list_leap_motions():
|
def list_leap_motions():
|
||||||
controller = Leap.Controller()
|
controller = Leap.Controller()
|
||||||
|
@ -16,13 +16,14 @@ def list_leap_motions():
|
||||||
|
|
||||||
|
|
||||||
class LeapMotion(Leap.Listener):
|
class LeapMotion(Leap.Listener):
|
||||||
def __init__(self, dsp, track=0, amplitude=.5, min_frequency=55, max_frequency=10000):
|
def __init__(self, dsp, track=0, amplitude=.5, min_frequency=55, max_frequency=10000, left_handed=False):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.dsp = dsp
|
self.dsp = dsp
|
||||||
self.track = track
|
self.track = track
|
||||||
self.amplitude = amplitude
|
self.amplitude = amplitude
|
||||||
self.min_frequency = min_frequency
|
self.min_frequency = min_frequency
|
||||||
self.max_frequency = max_frequency
|
self.max_frequency = max_frequency
|
||||||
|
self.left_handed = left_handed
|
||||||
self.controller = Leap.Controller()
|
self.controller = Leap.Controller()
|
||||||
self.controller.add_listener(self)
|
self.controller.add_listener(self)
|
||||||
|
|
||||||
|
@ -44,15 +45,11 @@ class LeapMotion(Leap.Listener):
|
||||||
|
|
||||||
if len(frame.hands) > 0:
|
if len(frame.hands) > 0:
|
||||||
if len(frame.hands) == 1:
|
if len(frame.hands) == 1:
|
||||||
left = frame.hands[0]
|
left = frame.hands[0] if frame.hands[0].is_left else None
|
||||||
right = None
|
right = frame.hands[0] if frame.hands[0].is_right else None
|
||||||
else:
|
else:
|
||||||
if frame.hands[0].palm_position[0] < frame.hands[1].palm_position[0]:
|
left = frame.hands[0] if frame.hands[0].is_left else frame.hands[1]
|
||||||
left = frame.hands[0]
|
right = frame.hands[1] if frame.hands[0].is_right else frame.hands[1]
|
||||||
right = frame.hands[1]
|
|
||||||
else:
|
|
||||||
left = frame.hands[1]
|
|
||||||
right = frame.hands[0]
|
|
||||||
|
|
||||||
return left, right
|
return left, right
|
||||||
|
|
||||||
|
@ -64,33 +61,37 @@ class LeapMotion(Leap.Listener):
|
||||||
self.dsp.stop(self.track)
|
self.dsp.stop(self.track)
|
||||||
return
|
return
|
||||||
|
|
||||||
y_left = left.palm_position[1]
|
y_left = left.palm_position[1] if left else None
|
||||||
|
y_right = right.palm_position[1] if right else None
|
||||||
|
frequency = None
|
||||||
|
amplitude = None
|
||||||
|
|
||||||
|
if self.left_handed:
|
||||||
|
if y_left:
|
||||||
frequency = self.y_to_freq(y_left)
|
frequency = self.y_to_freq(y_left)
|
||||||
|
if y_right:
|
||||||
|
amplitude = self.y_to_amplitude(y_right)
|
||||||
|
else:
|
||||||
|
if y_left:
|
||||||
|
amplitude = self.y_to_amplitude(y_left)
|
||||||
|
if y_right:
|
||||||
|
frequency = self.y_to_freq(y_right)
|
||||||
|
|
||||||
if right:
|
if frequency:
|
||||||
y_right = right.palm_position[1]
|
if self.dsp.discrete:
|
||||||
self.amplitude = self.y_to_amplitude(y_right)
|
frequency = midi_to_freq(freq_to_midi(frequency))
|
||||||
|
|
||||||
self.dsp.set_frequency(self.track, frequency)
|
self.dsp.set_frequency(self.track, frequency)
|
||||||
self.dsp.set_volume(self.track, self.amplitude)
|
|
||||||
|
|
||||||
if not self.dsp.is_playing(self.track):
|
if not self.dsp.is_playing(self.track):
|
||||||
self.dsp.play(self.track)
|
self.dsp.play(self.track)
|
||||||
|
|
||||||
print('Hand height: {:.8f}, Frequency: {:1f}, MIDI: {}, Amplitude: {:4f}'.format(
|
if amplitude:
|
||||||
y_left, frequency, self.freq_to_midi_note(frequency), self.amplitude))
|
self.dsp.set_volume(self.track, amplitude)
|
||||||
|
self.amplitude = amplitude
|
||||||
|
|
||||||
@staticmethod
|
print('Left hand height: {:.8f}, Right hand height: {:.8f}, Frequency: {:1f}, MIDI: {}, Amplitude: {:4f}'.
|
||||||
def midi_note_to_str(midi):
|
format(y_left or 0, y_right or 0, frequency or 0, freq_to_midi_str(frequency) if frequency else 0,
|
||||||
notes = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
|
self.amplitude))
|
||||||
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):
|
def y_to_freq(self, y):
|
||||||
y_min_height = 70
|
y_min_height = 70
|
||||||
|
@ -100,7 +101,6 @@ class LeapMotion(Leap.Listener):
|
||||||
|
|
||||||
y -= y_min_height
|
y -= y_min_height
|
||||||
y_max_height -= y_min_height
|
y_max_height -= y_min_height
|
||||||
# max_freq -= min_freq
|
|
||||||
|
|
||||||
frequency = min_freq + ((max_freq*y) / y_max_height)
|
frequency = min_freq + ((max_freq*y) / y_max_height)
|
||||||
if frequency < min_freq:
|
if frequency < min_freq:
|
||||||
|
|
|
@ -26,8 +26,9 @@ class Track:
|
||||||
|
|
||||||
|
|
||||||
class SoundProcessor:
|
class SoundProcessor:
|
||||||
def __init__(self, backend='portaudio', output=None, channels=2):
|
def __init__(self, backend='portaudio', output=None, channels=2, discrete=False):
|
||||||
self.backend = backend
|
self.backend = backend
|
||||||
|
self.discrete = discrete
|
||||||
self.server = Server(audio=self.backend, jackname='theremin', winhost='theremin', nchnls=channels)
|
self.server = Server(audio=self.backend, jackname='theremin', winhost='theremin', nchnls=channels)
|
||||||
self.audio_output = output or pa_get_default_output()
|
self.audio_output = output or pa_get_default_output()
|
||||||
self.server.setOutputDevice(self.audio_output)
|
self.server.setOutputDevice(self.audio_output)
|
||||||
|
@ -73,3 +74,4 @@ class SoundProcessor:
|
||||||
def set_frequency(self, i, frequency):
|
def set_frequency(self, i, frequency):
|
||||||
assert 0 <= i < len(self.tracks)
|
assert 0 <= i < len(self.tracks)
|
||||||
self.tracks[i].set_frequency(frequency)
|
self.tracks[i].set_frequency(frequency)
|
||||||
|
|
59
theremin/sound/utils.py
Normal file
59
theremin/sound/utils.py
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
import math
|
||||||
|
|
||||||
|
notes = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
|
||||||
|
base_c = 24
|
||||||
|
|
||||||
|
|
||||||
|
def midi_to_midi_str(midi):
|
||||||
|
"""
|
||||||
|
:param int midi: MIDI int value (e.g. 69)
|
||||||
|
:return: MIDI string (e.g. A4)
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
global notes, base_c
|
||||||
|
|
||||||
|
assert 24 <= midi <= 127
|
||||||
|
i = (midi - base_c) % len(notes)
|
||||||
|
octave = int((midi - base_c) / 12) + 1
|
||||||
|
return '{note}{octave}'.format(note=notes[i], octave=octave)
|
||||||
|
|
||||||
|
|
||||||
|
def freq_to_midi(freq):
|
||||||
|
"""
|
||||||
|
:param float freq: Frequency (e.g. 440.0)
|
||||||
|
:return: MIDI int value (e.g. 69)
|
||||||
|
:rtype: int
|
||||||
|
"""
|
||||||
|
return int(math.log2(freq / 440.0) * 12) + 69
|
||||||
|
|
||||||
|
|
||||||
|
def freq_to_midi_str(freq):
|
||||||
|
"""
|
||||||
|
:param float freq: Frequency (e.g. 440.0)
|
||||||
|
:return: MIDI string (e.g. A4)
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
return midi_to_midi_str(freq_to_midi(freq))
|
||||||
|
|
||||||
|
|
||||||
|
def midi_to_freq(midi):
|
||||||
|
"""
|
||||||
|
:param int midi: MIDI note (e.g. 69)
|
||||||
|
:return: Frequency in Hz (e.g. 440.0)
|
||||||
|
:rtype: float
|
||||||
|
"""
|
||||||
|
return 440.0 * math.pow(2.0, (midi - 69) / 12)
|
||||||
|
|
||||||
|
|
||||||
|
def midi_str_to_midi(midi_str):
|
||||||
|
"""
|
||||||
|
:param str midi_str: MIDI string (e.g. A4)
|
||||||
|
:return: MIDI int value (e.g. 69)
|
||||||
|
:rtype: int
|
||||||
|
"""
|
||||||
|
global notes, base_c
|
||||||
|
|
||||||
|
note = notes.index(midi_str[0].upper())
|
||||||
|
octave = int(midi_str[1])
|
||||||
|
assert 1 <= octave <= 9
|
||||||
|
return 12 + note + 12*octave
|
25
theremin/theremin.py
Normal file
25
theremin/theremin.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, discrete=False, left_handed=False):
|
||||||
|
dsp = SoundProcessor(output=audio_output, backend=audio_backend, channels=channels, discrete=discrete)
|
||||||
|
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,
|
||||||
|
left_handed=left_handed)
|
||||||
|
print('Press ENTER to quit')
|
||||||
|
sys.stdin.readline()
|
||||||
|
|
||||||
|
sensor.stop()
|
||||||
|
dsp.shutdown()
|
Loading…
Reference in a new issue