MIDI Based Theremin using a Raspberry Pi

This is a simple guide to build an electronic musical instrument. The MIDI notes are generated using a Raspberry Pi and a sonar sensor. Although the original Theremin is played using antennas, this one will use sonar technology as pitch control. The idea is to vary the distance between the user hand and the distance sensor, causing the pitch to change accordingly.



How will the measured distance (dist) translate to pitch?

This is done in the get_note() method.

def get_note(dist=0):
    """ Compute the note based on the distance measurements, get percentages of each scale and compare """
    # Config
    # you can play with these settings
    minDist = 3    # Distance Scale
    maxDist = 21
    octaves = 1
    minNote = 48   # c4 middle c
    maxNote = minNote + 12*octaves
    # Percentage formula
    fup = (dist - minDist)*(maxNote-minNote)
    fdown = (maxDist - minDist)
    note = minNote + fup/fdown
    """ To-do: calculate trends form historical data to get a smoother transitions """
    return int(note)

Like any other musical instrument, this one will translate a user action to sound. In this case the user changes the distance between the HC-SR04 sensor and an object (hand) producing a sound. The idea is to equally distribute the amount of notes through the distance scale, resulting in a playable area.


This is done using proportions. When dist = minDist the resulting note will be minNote. The same way is done on the other end of the scale. This enables the software to define the relation between the distance and the steps for each note in a function form. This function is evaluated every time a distance (dist) is measured.

It is assumed that the proportion of the measured distance (dist) should be equal to the proportion of the generated note (note).
$noteProportion  = distProportion$

Each proportion can be calculated using simple fractions.

$distProportion = \frac{dist - minDist}{maxDist - minDist}$
$noteProportion = \frac{note - minNote}{maxNote - minNote}$

The resulting formula to calculate the note.

$note\left ( dist \right ) = minNote + \frac{\left (dist - minDist\right )\times \left (maxNote - minNote\right )}{maxDist - minDist}$


NOTE: This is just one approach to define the interface of this instrument. Add-ons and changes can be made in order to get many different effects in the way this is played.

Just run or download the following code.

# Derick DeLeon
# 2014-02
# theremin.py
"""MIDI based theremin using a Raspberry Pi and a sonar HC-SR04 as pitch control"""
"""derickdeleon.com"""

import RPi.GPIO as GPIO
import pygame.midi
import time

# Distance Related Methods

def prepare(GPIO_ECHO, GPIO_TRIGGER):
    """ Initialize the Raspberry Pi GPIO  """
    # Set pins as output and input
    GPIO.setup(GPIO_TRIGGER,GPIO.OUT)    # Trigger
    GPIO.setup(GPIO_ECHO,GPIO.IN)        # Echo
    # Set trigger to False (Low)
    GPIO.output(GPIO_TRIGGER, False)
    # Allow module to settle
    time.sleep(0.5)

def get_distance(GPIO_ECHO, GPIO_TRIGGER):
    """ get the distance from the sensor, echo - the input from sensor """
    # Send 10us pulse to trigger
    GPIO.output(GPIO_TRIGGER, True)
    time.sleep(0.00001)
    GPIO.output(GPIO_TRIGGER, False)
    start = time.time()
    # Taking time
    while GPIO.input(GPIO_ECHO)==0:
        start = time.time()
    while GPIO.input(GPIO_ECHO)==1:
        stop = time.time()
    # Calculate pulse length
    elapsed = stop-start
    # Distance pulse travelled in that time is time
    # multiplied by the speed of sound (cm/s)
    distance = elapsed * 34300
    # That was the distance there and back so halve the value
    distance = distance / 2
    return distance

# Audio and MIDI Related Methods

def conf_midi():
    """ Initialize MIDI component """
    instrument = 79 # Whistle
    pygame.init()
    pygame.midi.init()
    port = 2
    global midiOutput   # It is used in other methods
    midiOutput = pygame.midi.Output(port, 1)
    midiOutput.set_instrument(instrument)

def play_midi(note, b4note, volume):
    """ Play a new MIDI note and turn off the last one """
    # use the last note to compare
    if (note != b4note):
        """ To-Do: smoother transitions between notes, use pitch bend. """
        midiOutput.note_off(b4note,volume)
        midiOutput.note_on(note,volume)
    # help to not consume all resources
    time.sleep(.15)

def get_note(dist=0):
    """ Compute the note based on the distance measurements, get percentages of each scale and compare """
    # Config
    # you can play with these settings
    minDist = 3    # Distance Scale
    maxDist = 21
    octaves = 1
    minNote = 48   # c4 middle c
    maxNote = minNote + 12*octaves
    # Percentage formula
    fup = (dist - minDist)*(maxNote-minNote)
    fdown = (maxDist - minDist)
    note = minNote + fup/fdown
    """ To-do: calculate trends form historical data to get a smoother transitions """
    return int(note)

# MAIN
GPIO.setwarnings(False)
# The pin number is the actual pin number
GPIO.setmode(GPIO.BCM)
# Set up the GPIO channels
trigger = 17
echo = 27
prepare(echo, trigger)
note = 0
conf_midi()
volume = 127
try:
    while True:
        b4note = note
        # get distance
        d = get_distance(echo, trigger)
        # calculate note
        note = get_note(d)
        # to-do, take a number of sample notes and average them, or any other statistical function. eg. Play just minor notes, ponderate average, etc. 
        # play the note
        play_midi(note, b4note, volume)
except KeyboardInterrupt:
    GPIO.cleanup()
    del midiOutput
    pygame.midi.quit()
GPIO.cleanup()
pygame.midi.quit()

Here's a sound sample of the MIDI Theremin in action or download from here:


Please see my previous posts for detailed information on wiring and operation.

MIDI and the Raspberry Pi
Distance sensor working on a Raspberry Pi





Please visit and thanks to:
A nice story about theremins in pop culture: The Real Instrument Behind The Sound In 'Good Vibrations'


24 comments:

  1. Thanks for your great tutorial. I'm getting the following errors when running the script:

    File "theremin.py", line 91, in
    conf_midi()
    File "theremin.py", line 53, in conf_midi
    midiOutput = pygame.midi.Output(port, 1)
    File "/usr/lib/python2.7/dist-packages/pygame/midi.py", line 414, in __init__
    raise MidiException("Device id invalid, out of range.")
    pygame.midi.MidiException: 'Device id invalid, out of range.'

    Any suggestions?

    ReplyDelete
    Replies
    1. Thanks for your comment, I'm glad you liked my tutorial.

      It looks to me that the port number is wrong. In my setup the port number that worked was port number 2, please try other integers (1,3,etc).

      See https://www.pygame.org/docs/ref/midi.html#pygame.midi.Output for more info on this function.

      Please let me know if that works for you.

      D

      Delete
    2. Thanks for pointing me in the right direction. I no longer get that error after setting my port to 0 and I can hear the "hiss" of sound coming through the speakers, but I can't get any actual tones.

      The script found here works and gives me a reliable distance, so I know that I've got everything set up right.
      http://www.modmypi.com/blog/hc-sr04-ultrasonic-range-sensor-on-the-raspberry-pi

      Just can't get it to play any tones.

      Any other suggestions? I appreciate all of your help so far!

      Delete
    3. Have you tried my post on MIDI and Rapsberry Pi?
      http://www.derickdeleon.com/2014/02/midi-and-raspberry-pi.html

      I think that the problem is on the MIDI side. Please take a look to that post and let me know if you can hear something using that script.

      Kind Regards,
      D

      Delete
    4. Thanks for your reply and for pointing me to your other post. It does seem like the problem is on the midi side. Can't hear anything with your other script either. Is there anything else I need to do to get midi up and running on the pi? I'm using a B+. I couldn't find much via google. Thanks again for all of your help. Hoping I can get this running to impress my students.

      Delete
    5. Can you please try this code?

      for id in range(pygame.midi.get_count()):
      print pygame.midi.get_device_info(id)

      Let's see the output please.

      D

      Delete
    6. Here is a nice explanation on the pygame.midi module

      http://tinyurl.com/norxpud

      Hope it helps, please let me know.

      D

      Delete
  2. Thanks for the links they were very helpful.

    I tried the code and got the following output:

    ('ALSA', 'Midi Through Port-0', 0, 1, 0)
    ('ALSA', 'Midi Through Port-0', 1, 0, 0)

    Then I tried changing the port to 0 on your midi tutorial (midihelloworld.py) and the script ran without error but no sound.

    ReplyDelete
    Replies
    1. I will try to reproduce the error as soon as I get home from work.

      In the meantime, are you listening form the headphones jack or the HDMI audio?

      D

      Delete
  3. I got it to work. I had to first run timidity as a server and then point your script to it for output. Thanks so much for your help the last few days.

    Here's the command I used:

    timidity -iA -B2,8 -Os -EFreverb=0

    ReplyDelete
    Replies
    1. AWESOME!!! I am glad it finally worked.

      Please feel free to ask any question any time.

      D

      Delete
    2. What exactly do you mean with " point your script to it for output" ? How do you do that ?

      Delete
    3. I guess @grainybazzles ment to chanhe the script in a way that the output sound would get out using timidity server instead of the ALSA one. I personally haven't tried this, but I guess is a good fix.

      Are you having issues?

      D

      Delete
    4. Hi how do you do "run timidity as a server"?
      Once it is downloaded, what do you do?

      Delete
    5. I hope this post helps you. https://vsr.informatik.tu-chemnitz.de/~jan/nted/doc/ch01s51.html

      Let me know how it went.

      D

      Delete
    6. hmm not quite sure what to do after typing timidity -iA -B2,8 -Os1l -s 44100
      how do I get the code tp play through timidity?

      Thanks,
      Mizu

      Delete
  4. DUDE help me

    Traceback (most recent call last):
    File "theremin.py", line 91, in
    conf_midi()
    File "theremin.py", line 53, in conf_midi
    midiOutput = pygame.midi.Output(port, 1)
    File "/usr/lib/python2.7/dist-packages/pygame/midi.py", line 414, in __init__
    raise MidiException("Device id invalid, out of range.")
    pygame.midi.MidiException: 'Device id invalid, out of range.'
    ___________________________________________________________________

    İ connect sensor like this

    http://www.raspberrypi-spy.co.uk/2012/12/ultrasonic-distance-measurement-using-python-part-1/

    ReplyDelete
    Replies
    1. It looks like your port is invalid. Please check my MIDI post about how to get MIDI and RasPi work.

      Please let me know how it went.

      D

      Delete
  5. http://www.derickdeleon.com/2014/02/midi-and-raspberry-pi.html


    # This port number seems the only one to work
    port = 2
    latency = 1


    i try port "0"

    no error but no sound
    check rpi sound out "raspi-config" its ok
    check C-SR04-its ok

    ReplyDelete
  6. DUDE help me
    I 'm just a simple music teacher from east

    HC-SR04_example.py work good but ;
    python theremin.py not work no error but no sound .any idea ?

    maybe u upload raspberry pi image wetransfer*com or other share site , I need it for my poor students


    ReplyDelete
    Replies
    1. I will upload an image as soon as I can (I guess it is not going to be fast). In the meantime, the RasPi can output audio through the HDMI connection, have you tried to connect a TV with speakers?

      Have you tried the MIDI solo exercise in this blog? If you did, did you got any errors?


      Please let me know how it went.

      D

      Delete
    2. yes i try hdmı and anlolog out no sound

      Delete
  7. I have problem making this code work on my project , tried to adapt it to my build and I still get the following error:
    File "Theremin.py", line 56, in conf_midi()
    File "Theremin.py", line 31, in conf_midi
    midiOutut = pygame.midi.Output(port, 1)
    File "/usr/lib/python2.7/dist-package/pygame/midi.py", line 414, in __init__pygame.midi.MidiException("Device id invalid, out of range."

    ReplyDelete
  8. you can open another terminal, and start the timidity server...

    > timidity -iA

    that will get it running in interactive mode, so go back to the original terminal session and run your python script. If you want timidity to always run on bootup, edit cron and add the command there

    > crontab -e

    then add

    sudo timidity -iA

    ReplyDelete