13

LASER AUDIO DISPLAY

Image

In Chapter 12, you learned the basics of the Arduino, which is perfect for interfacing with low-level electronic devices. In this project, you’ll leverage the Arduino to build hardware to produce interesting laser patterns from audio signals. This time, Python will do more of the heavy lifting. In addition to handling serial communications, it will perform computations based on real-time audio data and use that data to adjust the motors in a laser display rig.

For these purposes, think of a laser as an intense beam of light that remains focused in a tiny point, even when projected over a large distance. This focus is possible because the beam is organized so that its waves travel in one direction only and are in phase with each other. For this project, you’ll use an inexpensive, easily obtainable laser pointer to create a laser pattern that changes in sync to music (or any audio input). You’ll build hardware that creates interesting patterns using the laser pointer and two rotating mirrors attached to motors. You’ll use the Arduino to set the direction and rotational speed of the motors, which you’ll control with Python via the serial port. The Python program will read audio input, analyze it, and convert it to motor speed and direction data to control the motors. You’ll also learn how to set the speed and direction of the motors to sync the patterns with music.

In this project, you will push your Arduino and Python knowledge further. Here are some of the topics we’ll cover:

• Generating interesting patterns with a laser and two rotating mirrors

• Getting frequency information from a signal using fast Fourier transform

• Computing fast Fourier transform using numpy

• Reading audio data using pyaudio

• Setting up serial communications between a computer and an Arduino

• Driving motors with an Arduino

Generating Patterns with a Laser

To generate the laser patterns in this project, you’ll use a laser pointer and two mirrors attached to the shafts of two small DC motors as shown in Figure 13-1. If you shine a laser at the surface of the flat mirror (mirror A), the reflection projected will remain a point, even if the motor is spinning. Because the plane of reflection of the laser is perpendicular to the spinning axis of the motor, it’s as if the mirror is not rotating at all.

Now, say the mirror is attached at an angle to the shaft, as shown on the right in Figure 13-1 (mirror B). As the shaft rotates, the projected point will trace an ellipse, and if the motor is spinning fast enough, the viewer will perceive the moving dot as a continuous shape.

Image

Figure 13-1: The flat mirror (mirror A) reflects a single dot. The reflection off the slanted mirror (mirror B) creates a circle as the motor spins.

What if you arrange the mirrors so that the point reflected off mirror A is projected onto mirror B? Now when motors A and B spin, the pattern created by the reflected point will be a combination of the two rotational movements of motors A and B, producing interesting patterns, as shown in Figure 13-2.

Image

Figure 13-2: Reflecting laser light off two rotating, slanted mirrors produces interesting, complex patterns.

The exact patterns produced will depend on the speed and direction of rotation of the two motors, but they will be similar to the hypotrochoids produced by the Spirograph you explored in Chapter 2.

Motor Control

You’ll use the Arduino to control the speed and direction of your motors. This setup requires some care to make sure it can take the relatively high voltage of the motors, because the Arduino can handle only so much current before it is damaged. You can protect the Arduino, simplify the design, and reduce development time by using the SparkFun TB6612FNG peripheral breakout board shown in Figure 13-3(a). Use the breakout board to control two motors simultaneously from an Arduino.

Image

Figure 13-3: SparkFun Motor Driver 1A Dual TB6612FNG

Figure 13-3(b) shows the soldered backside of the breakout board. The A and B in the pin names denote the two motors. The IN pins control the direction of the motors, the 01 and 02 pins supply power to the motors, and the PWM pins control the motor speeds. By writing to these pins, you can control both the direction and speed of rotation for each motor, which is exactly what you need for this project.

NOTE

You could replace this breakout part with any motor control circuit you’re familiar with, as long as you modify the Arduino sketch appropriately.

The Fast Fourier Transform

Because the ultimate goal in this project is to control motor speeds based on audio input, you need to be able to analyze the frequency of the audio.

Recall from Chapter 4 that tones from an acoustic instrument are a mix of several frequencies or overtones. In fact, any sound can be decomposed into its constituent frequencies using Fourier transforms. When the Fourier transform is applied to digital signals, the result is called the discrete Fourier transform (DFT) because digital signals are comprised of many discrete samples. In this project, you’ll use Python to implement a fast Fourier transform (FFT) algorithm to compute the DFT. (Throughout this chapter I’ll use FFT to refer to both the algorithm and the result.)

Here is a simple example of an FFT. Figure 13-4 shows a signal that combines just two sine waves, with the corresponding FFT below it. The wave at the top can be expressed by the following equation, which sums the two waves:

y(t) = 4sin(2π10t) + 2.5sin(2π30t)

Notice the 4 and 10 in the expression for the first wave—4 is the amplitude of the wave, and 10 is the frequency (in Hertz) of the wave. Meanwhile, the second wave has an amplitude of 2.5 and a frequency of 30.

The FFT reveals the wave’s component frequencies and their relative amplitude, showing peaks at 10 Hz and 30 Hz. The intensity of the first peak is about twice that of the second peak.

Image

Figure 13-4: An audio signal captured from music (top) and its corresponding FFT (bottom)

Now let’s look at a more complex example. Figure 13-5 shows an audio signal in the top frame and the corresponding FFT in the bottom frame.

Image

Figure 13-5: The FFT algorithm takes an amplitude signal (top) and computes its component frequencies (bottom).

The audio input, or signal, is in the time domain because the amplitude data varies with time. The FFT is in the frequency domain. Notice in the figure that the FFT displays a series of peaks showing the intensities of various frequencies in the signal.

To compute an FFT, you need a set of samples. The choice of the number of samples is a bit arbitrary, but a small sample size would not give you a good picture of the signal’s frequency content and might also mean a higher computational load because you would need to compute more FFTs per second. On the other hand, a sample size that’s too large would average out the changes in the signal, so you wouldn’t be getting a “real-time” frequency response for the signal. For the sampling rate of 44100 Hz used for this project, a sample size of 2048 represents data for about 0.046 seconds.

For this project, you need to split the audio data into its constituent frequencies and use that information to control the motors. First, you’ll split the range of frequencies (in Hz) into three bands: [0, 100], [100, 1000], and [1000, 2500]. You’ll compute an average amplitude for each band, and each value will affect the motors and resulting laser pattern differently, as follows:

• Changes in the average amplitude of low frequencies will affect the speed of the first motor.

• Changes in the average amplitude of middle frequencies will affect the speed of the second motor.

• When high frequencies peak above a certain threshold, the first motor will change direction.

Requirements

Here’s a list of the items you’ll need to build this project:

• A small laser pointer

• Two DC motors like the ones used in a small toy (rated for 9V)

• Two small mirrors approximately 1 inch or less in diameter

• A SparkFun Motor Driver 1A Dual TB6612FNG

• An Arduino Uno or similar board

• Wire to make connections (single-core hookup wires with male pins on both sides work nicely)

• A four AA battery pack

• Some LEGO bricks to raise the motors and laser pointer off the mounting board so the mirrors can spin freely

• A rectangular sheet of cardboard or acrylic about 8 inches × 6 inches to mount the hardware

• A hot glue gun

• Soldering iron

Constructing the Laser Display

The first order of business is to attach the mirrors to the motors. The mirror has to be at a slight angle to the motor shaft. To attach the mirror, place it facedown on a flat surface and put a drop of hot glue in the center. Carefully dip the motor shaft in the glue, keeping it perpendicular to the mirror until the glue hardens (see Figure 13-6). To test it, spin the mirror with your hand while shining the laser pointer at it. You should find the reflection of the laser dot moves in an ellipse when projected on a flat surface. Do the same for the second mirror.

Image

Figure 13-6: Attach the mirrors to each motor shaft at a slight angle.

Aligning the Mirrors

Next, align the laser pointer with the mirrors so that the laser reflects from mirror A to B, as shown in Figure 13-7. Be sure that the reflected laser light from mirror A stays within the circumference of mirror B for mirror A’s entire range of rotation. (This will take some trial and error.) To test the arrangement, manually rotate mirror A. Also, be sure to position mirror B so that the light reflected from its surface will fall on a flat surface (like a wall) for the full range of rotation of both the mirrors.

Image

Figure 13-7: The alignment of the laser and the mirrors

NOTE

As you tweak things, you will need to keep the laser pointer on. If your laser pointer has an on button, tape it down to keep the laser pointer on. (Or see “Experiments!” on page 271 for a more elegant way to control the power of the laser pointer.)

Once you’re happy with the placement of the mirrors, hot glue the laser pointer and the two motors with attached mirrors onto three identical blocks to raise them up so that they’ll be able to rotate freely. Next, place the blocks on the mounting board, and when you’re happy with their arrangement, mark the location of each by tracing their edge with a pencil. Then glue the blocks onto the board.

Powering the Motors

If your motors did not come with wires attached to their terminals (most don’t), solder wires to both terminals, being sure to leave sufficient wire (say 6 inches) so that you can attach the motors to the motor driver board. The motors are powered by four AA batteries in a battery pack, which you can hot glue to the back of the mounting board, as shown in Figure 13-8.

Image

Figure 13-8: Glue the battery pack to the back of the mounting board.

Now test the hardware by spinning both mirrors with your hands as the laser shines on them. If you spin them fast enough, you should see some interesting patterns emerging in a glimpse of what’s to come!

Wiring the Motor Driver

In this project, you’ll use the Sparkfun Motor Driver (TB6612FNG) to control the motors with the Arduino. I won’t go into the details of how this board works, but if you’re curious, you can start by reading up on an H bridge, a common circuit design that uses metal-oxide-semiconductor field-effect transistors (MOSFETs) to control motors.

Now you’ll connect the motors to the SparkFun motor driver and the Arduino. There are quite a few wires to connect, as listed in Table 13-1. Label one motor A and the other B, and keep to this convention when wiring them.

Table 13-1: SparkFun Motor Driver to Arduino Wiring

From

To

Arduino Digital Pin 12

TB6612FNG Pin BIN2

Arduino Digital Pin 11

TB6612FNG Pin BIN1

Arduino Digital Pin 10

TB6612FNG Pin STBY

Arduino Digital Pin 9

TB6612FNG Pin AIN1

Arduino Digital Pin 8

TB6612FNG Pin AIN2

Arduino Digital Pin 5

TB6612FNG Pin PWMB

Arduino Digital Pin 3

TB6612FNG Pin PWMA

Arduino 5V Pin

TB6612FNG Pin VCC

Arduino GND

TB6612FNG Pin GND

Arduino GND

Battery Pack GND (–)

Battery Pack VCC (+)

TB6612FNG Pin VM

Motor #1 Connector #1 (polarity doesn’t matter)

TB6612FNG Pin A01

Motor #1 Connector #2 (polarity doesn’t matter)

TB6612FNG Pin A02

Motor #2 Connector #1 (polarity doesn’t matter)

TB6612FNG Pin B01

Motor #2 Connector #2 (polarity doesn’t matter)

TB6612FNG Pin B02

Arduino USB connector

Computer’s USB port

Figure 13-9 shows everything wired up.

Image

Figure 13-9: The completely wired laser display

Now let’s work on the Arduino sketch.

The Arduino Sketch

You’ll start the sketch by setting up the digital output pins of the Arduino. Then, in the main loop, you will read data coming in via the serial port and convert the data into parameters that need to be sent to the motor driver board. You’ll also look at how to implement speed and direction control for the motors.

Configuring the Arduino’s Digital Output Pins

First, map the Arduino’s digital pins to the pins on the motor driver according to Table 13-1 and set the pins as outputs.

   // motor A connected to A01 and A02
   // motor B connected to B01 and B02

 int STBY = 10; //standby

   // Motor A
   int PWMA = 3; //speed control
   int AIN1 = 9; //direction
   int AIN2 = 8; //direction

   // Motor B
   int PWMB = 5; //speed control
   int BIN1 = 11; //direction 
 int BIN2 = 12; //direction

   void setup(){

     pinMode(STBY, OUTPUT);

       pinMode(PWMA, OUTPUT);
       pinMode(AIN1, OUTPUT);
       pinMode(AIN2, OUTPUT);

       pinMode(PWMB, OUTPUT);
       pinMode(BIN1, OUTPUT);
       pinMode(BIN2, OUTPUT);

       // initialize serial communication 
     Serial.begin(9600);
   }

From to , you map the names of the Arduino pins to the motor driver pins. For example, PWMA (Pulse With Modulation A) controls the speed of motor A and is assigned to Arduino pin 3. PWM is a way to power a device by sending digital pulses that switch on and off quickly such that the device “sees” a continuous voltage. The fraction of time that the digital pulse is on is called the duty cycle and is expressed as a percentage. By changing this percentage, you can provide varying power levels to a device. PWM is often used to control dimmable LEDs and motor speeds.

Then, you call the setup() method at , and in the lines that follow, you set all seven digital pins as output. At , you start serial communication, reading in serial data sent by the computer on the Arduino.

The Main Loop

The main loop in the sketch waits for serial data to arrive, parses it to extract the motor speed and direction, and uses that information to set the digital outputs for the driver board that controls the motors.

   // main loop that reads the motor data sent by laser.py
   void loop()
   {
        // data sent is of the form 'H' (header), speed1, dir1, speed2, dir2
      if (Serial.available() >= 5) {
          if(Serial.read() == 'H') {
                // read the next 4 bytes
              byte s1 = Serial.read();
                byte d1 = Serial.read();
                byte s2 = Serial.read();
                byte d2 = Serial.read();

                // stop the motor if both speeds are 0
              if(s1 == 0 && s2 == 0) {
                    stop();
                }
                else {
                    // set the motors' speed and direction
                  move(0, s1, d1);
                    move(1, s2, d2);
                }
                // slight pause for 20 ms
              delay(20);
            }
            else {
                // if there is invalid data, stop the motors
              stop();
           }
       }
       else {
           // if there is no data, pause for 250 ms
         delay(250);
       }
   }

The motor control data is sent as a set of 5 bytes: H followed by 4 single-byte numbers, s1, d1, s2, and d2, which represent the speed and direction of the motors. Since serial data comes in continuously, at , you check to ensure that you have received at least 5 bytes. If not, you delay for 250 milliseconds and try to read the data again in the next cycle.

At , you check that the first byte you read in is an H to ensure that you’re at the beginning of a proper set of control data and that the next 4 bytes are what you expect them to be. If not, you stop the motors at because the data may have been corrupted by transmission or connection errors.

Beginning at , the sketch reads the speed and direction data for the two motors. If both motor speeds are set to zero, stop the motors . If not, the speed and direction values are assigned to the motors at , using the move() method. At , you add a small delay in data reading to allow the motors to keep up and to make sure you’re not reading in data too fast.

Here is the move() method used to set the speed and direction of the motors:

   // set motor speed and direction
   // motor: A -> 1, B -> 0
   // direction: 1/0
   void move(int motor, int speed, int direction)
   {
        // disable standby
      digitalWrite(STBY, HIGH);

      boolean inPin1 = LOW;
        boolean inPin2 = HIGH;

      if(direction == 1){
            inPin1 = HIGH;
            inPin2 = LOW;
        }

       if(motor == 1){
         digitalWrite(AIN1, inPin1);
           digitalWrite(AIN2, inPin2);
           analogWrite(PWMA, speed);
       }
       else{
         digitalWrite(BIN1, inPin1);
           digitalWrite(BIN2, inPin2);
           analogWrite(PWMB, speed);
       }
   }

The motor driver has a standby mode to save power when the motors are off. You leave standby by writing HIGH to the standby pin at . At , you define two Boolean variables, which determine the direction of rotation of the motors. At , if the direction argument is set to 1, you flip the values of these variables, which allows you to switch the motor’s direction in the code that follows.

You set the pins AIN1, AIN2, and PWMA for motor A at . Pins AIN1 and AIN1 control the motor’s direction, and you use the Arduino digitalWrite() method to set one pin to HIGH (1) and one to LOW (0) as needed. In the case of pin PWMA, you send the PWM signal, which allows you to control the motor’s speed, as described earlier. To control the value of the PWM, you use the analogWrite() method to write a value in the range [0, 255] to an Arduino output pin. (In contrast, the digitalWrite() method only lets you write either a 1 or 0 to the output pin.)

At , you set the pins for motor B.

Stopping the Motors

To stop the motors, you write a LOW to the standby pin of the motor driver.

void stop(){
    //enable standby
    digitalWrite(STBY, LOW);
}

The Python Code

Now let’s look at the Python code running on the computer. This code does the heavy lifting: it reads in audio, computes the FFT, and sends serial data to the Arduino. You can find the complete project code in “The Complete Python Code” on page 267.

Selecting the Audio Device

First, you need to read in the audio data with the help of the pyaudio module. Initialize the pyaudio module like this:

p = pyaudio.PyAudio()

Next, you access the computer’s audio input device using the helper functions in pyaudio, as shown in the code for the getInputDevice() method:

   # get pyaudio input device
   def getInputDevice(p):
      index = None
      nDevices = p.get_device_count()
        print('Found %d devices. Select input device:' % nDevices)
        # print all devices found
        for i in range(nDevices):
          deviceInfo = p.get_device_info_by_index(i)
          devName = deviceInfo['name']
          print("%d: %s" % (i, devName))
        # get user selection
        try:
            # convert to integer
           index = int(input())
        except:
            pass

        # print the name of the chosen device
        if index is not None:
            devName = p.get_device_info_by_index(index)["name"]
            print("Input device chosen: %s" % devName)
      return index

At , you set an index variable to None. (This index is the return value for the function at , and if it is returned as None, you know that no suitable input device was found.) At , you use the get_device_count() method to get the number of audio devices on the computer, including any audio hardware such as microphones, line inputs, or line outputs. You then iterate through all found devices, getting information about each.

The get_device_info_by_index() function at returns a dictionary containing information about various features of each audio device, but you’re interested only in the name of the device because you’re looking for an input device. You store the device name at , and at , you print out the index and name of the device. At , you use the input() method to read the selection from the user, converting the string read in to an integer index. At , this selected index is returned from the function.

Reading Data from the Input Device

Once you have selected the input device, you need to read data from it. To do so, you first open the audio stream, as shown here. (Note that all the code runs continuously in a while loop.)

      # set FFT sample length
    fftLen = 2**11
      # set sample rate
    sampleRate = 44100

      print('opening stream...')
    stream = p.open(format = pyaudio.paInt16,
                      channels = 1,
                      rate = sampleRate,
                      input = True,
                      frames_per_buffer = fftLen,
                      input_device_index = inputIndex)

At , you set the length of the FFT buffer—the number of audio samples you will use to compute the FFT—to 2048 (which is 211; FFT algorithms are optimized for powers of 2). Then, you set the sampling rate for pyaudio to 44100 or 44.1 kHz , which is standard for CD-quality recordings.

Next, you open the pyaudio stream and specify several options:

pyaudio.paInt16 indicates that you’re reading in the data as 16-bit integers.

channels is set to 1 because you’re reading the audio as a single channel.

rate is set to the chosen sample rate of 44100 Hz.

input is set to True.

frames_per_buffer is set to the FFT buffer length.

input_device_index is set to the device you chose in the getInputDevice() method.

Computing the FFT of the Data Stream

Here is the code you use to read data from the stream:

        # read a chunk of data
       data = stream.read(fftLen)
        # convert the data to a numpy array
       dataArray = numpy.frombuffer(data, dtype=numpy.int16)

At , you read the most recent fftLen samples from the audio input stream. Then you convert this data into a 16-bit integer numpy array at .

Now you compute the FFT of this data.

        # get FFT of data
       fftVals = numpy.fft.rfft(dataArray)*2.0/fftLen
        # get absolute values of complex numbers
       fftVals = numpy.abs(fftVals)

At , you compute the FFT of the values in the numpy array, using the rfft() method from the numpy fft module. This method takes a signal composed of real numbers (like the audio data) and computes the FFT, which generally results in a set of complex numbers. The 2.0/fftLen is a normalization factor you use to map the FFT values to the expected range. Then, because the rfft() method returns complex numbers, you use the numpy abs() method to get the magnitudes of these complex numbers, which are real.

Extracting Frequency Information from the FFT Values

Next, you extract the relevant frequency information from the FFT values.

        # average 3 frequency bands: 0-100 Hz, 100-1000 Hz, and 1000-2500 Hz
        levels = [numpy.sum(fftVals[0:100])/100,
                  numpy.sum(fftVals[100:1000])/900,
                  numpy.sum(fftVals[1000:2500])/1500]

To analyze the audio signal, you split the frequency range into three bands: 0 to 100 Hz, 100 to 1000 Hz, and 1000 to 2500 Hz. You are most interested in the lower, bass band (0–100 Hz) and the midrange (100–1000 Hz) frequencies, which roughly correspond to the beat and the vocals in a song, respectively. For each range, you compute the average FFT value using the numpy.sum() method in the code.

Converting Frequency to Motor Speed and Direction

Now convert this frequency information to motor speeds and directions.

        # 'H' (header), speed1, dir1, speed2, dir2
       vals = [ord('H'), 100, 1, 100, 1]
        # speed1
       vals[1] = int(5*levels[0]) % 255
        # speed2
       vals[3] = int(100 + levels[1]) % 255

        # dir
        d1 = 0
       if levels[2] > 0.1:
            d1 = 1
        vals[2] = d1
       vals[4] = 0

At , you initialize a list of motor speeds and direction values to be sent to the Arduino (the 5 bytes, starting with H discussed earlier). Use the builtin ord() function to convert the string to an integer and then fill in this list by converting the average values for the three frequency bands into motor speeds and directions.

NOTE

This part is a hack, really—there’s no particularly elegant rule governing these conversions. These values change constantly with the audio signal, and any method you come up with will change the motor speeds and affect the laser pattern along with the music. Just make sure your conversion puts the motor speeds in the [0, 255] range and that the directions are always set to 1 or 0. The method I chose was simply based on trial and error; I looked at FFT values while playing various types of music.

At , you take the value from the lowest frequency range, scale it by a factor of five, convert it to an integer, and use the modulus operator (%) to ensure that the value lies within the [0, 255] range. This value controls the speed of the first motor. At , you add 100 to the middle frequency value and place it in the [0, 255] range. This value controls the speed of the second motor.

Then, at , you switch the motor A direction whenever the value from the highest frequency range crosses the threshold of 0.1. The motor B direction is kept at a constant 0 . (I found through trial and error that these methods produce a nice variation of patterns, but I encourage you to play with these values and create your own conversions. There are no wrong answers here.)

Testing the Motor Setup

Before testing the hardware with a live audio stream, let’s check the motor setup. The function autoTest(), shown here, does just that:

   # automatic test for sending motor speeds
   def autoTest(ser):
       print('starting automatic test...')
       try:
           while True:
           # for each direction combination
         for dr in [(0, 0), (1, 0), (0, 1), (1, 1)]:
               # for a range of speeds
              for j in range(25, 180, 10):
                  for i in range(25, 180, 10):
                      vals = [ord('H'), i, dr[0], j, dr[1]]
                      print(vals[1:])
                      data = struct.pack('BBBBB', *vals)
                      ser.write(data)
                        sleep(0.1)
   except KeyboardInterrupt:
       print('exiting...')
       # shut off motors
     vals = [ord('H'), 0, 1, 0, 1]
       data = struct.pack('BBBBB', *vals)
       ser.write(data)
       ser.close()

This method takes the two motors through a range of motions by varying the speed and direction of each. Because the direction can be clockwise or counterclockwise for each motor, four such combinations are represented in the outer loop at . For each combination, the loops at and run the motors at various speeds.

NOTE

I’m using range(25, 180, 10), which means the speed varies from 25 to 180 in steps of 10. I am not using the full range of motion of the motor [0, 255] here because the motors barely turn below a speed of 25 and they spin really fast above 200.

At , you generate the 5-byte motor data values, and at , you print their direction and speed values. (The use of Python string splicing vals[1:] will get all but the first element in the list.)

Pack the motor data into a byte array at , and write it to the serial port at . Pressing CTRL-C interrupts this test, and at , you handle this exception by cleaning up, stopping the motors, and closing the serial port like the responsible programmer you are.

Command Line Options

As with previous projects, you use the argparse module to parse command line arguments for the program.

# main method
def main():
    # parse arguments
    parser = argparse.ArgumentParser(description='Analyzes audio input and
sends motor control information via serial port')
    # add arguments
    parser.add_argument('--port', dest='serial_port_name', required=True)
    parser.add_argument('--mtest', action='store_true', default=False)
    parser.add_argument('--atest', action='store_true', default=False)
    args = parser.parse_args()

In this code, the serial port is a required command line option. There are also two optional command line options: one for an automatic test (covered earlier) and another for a manual test (which I’ll discuss shortly).

Here is what happens once the command line options are parsed in the main() method:

      # open serial port
      strPort = args.serial_port_name
      print('opening ', strPort)
     ser = serial.Serial(strPort, 9600)
      if args.mtest:
          manualTest(ser)
      elif args.atest:
          autoTest(ser)
      else:
        fftLive(ser)

At , you use pySerial to open a serial port with the string passed in to the program. The speed of serial communications, or the baud rate, is set at 9,600 bits per second. If no other command arguments (--atest or --mtest) are used, you proceed at to the audio processing and FFT computation, encapsulated in the fftLive() method.

Manual Testing

This manual test lets you enter specific motor directions and speeds so that you can see their effects on the laser pattern.

   # manual test of motor direction and speeds
   def manualTest(ser):
       print('starting manual test...')
       try:
           while True:
               print('enter motor control info such as < 100 1 120 0 >')
              strIn = raw_input()
              vals = [int(val) for val in strIn.split()[:4]]
              vals.insert(0, ord('H'))
              data = struct.pack('BBBBB', *vals)
              ser.write(data)
      except:
          print('exiting...')
          # shut off the motors
        vals = [ord('H'), 0, 1, 0, 1]
          data = struct.pack('BBBBB', *vals)
          ser.write(data)
          ser.close()

At , you use the raw_input() method to wait until the user enters a value at the command prompt. The expected entry is in the form 100 1 120 0, representing the speed and direction of motor A followed by that of motor B. Parse the string into a list of integers at . At , you insert an 'H' to complete the motor data, and at and , you pack this data and send it through the serial port in the expected format. When the user interrupts the test using CTRL-C (or if any exception occurs), you clean up at by shutting down the motors and the serial port gracefully.

The Complete Python Code

Here is the complete Python code for this project. You’ll can also find this at https://github.com/electronut/pp/tree/master/arduino-laser/laser.py.

import sys, serial, struct
import pyaudio
import numpy
import math
from time import sleep
import argparse

# manual test of motor direction speeds
def manualTest(ser):
    print('staring manual test...')
    try:
        while True:
            print('enter motor control info: eg. < 100 1 120 0 >')
            strIn = raw_input()
            vals = [int(val) for val in strIn.split()[:4]]
            vals.insert(0, ord('H'))
            data = struct.pack('BBBBB', *vals)
            ser.write(data)
    except:
        print('exiting...')
        # shut off motors
        vals = [ord('H'), 0, 1, 0, 1]
        data = struct.pack('BBBBB', *vals)
        ser.write(data)
        ser.close()

# automatic test for sending motor speeds
def autoTest(ser):
    print('staring automatic test...')
    try:
        while True:
            # for each direction combination
            for dr in [(0, 0), (1, 0), (0, 1), (1, 1)]:
                # for a range of speeds
                for j in range(25, 180, 10):
                    for i in range(25, 180, 10):
                        vals = [ord('H'), i, dr[0], j, dr[1]]
                        print(vals[1:])
                        data = struct.pack('BBBBB', *vals)
                        ser.write(data)
                        sleep(0.1)
    except KeyboardInterrupt:
        print('exiting...')
        # shut off motors
        vals = [ord('H'), 0, 1, 0, 1]
        data = struct.pack('BBBBB', *vals)
        ser.write(data)
        ser.close()

    # get pyaudio input device
    def getInputDevice(p):
        index = None
        nDevices = p.get_device_count()
        print('Found %d devices. Select input device:' % nDevices)
        # print all devices found
        for i in range(nDevices):
            deviceInfo = p.get_device_info_by_index(i)
            devName = deviceInfo['name']
            print("%d: %s" % (i, devName))
        # get user selection
        try:
            # convert to integer
            index = int(input())
        except:
            pass

        # print the name of the chosen device
        if index is not None:
            devName = p.get_device_info_by_index(index)["name"]
            print("Input device chosen: %s" % devName)
        return index

    # FFT of live audio
    def fftLive(ser):
        # initialize pyaudio
        p = pyaudio.PyAudio()

        # get pyAudio input device index
        inputIndex = getInputDevice(p)

        # set FFT sample length
        fftLen = 2**11
        # set sample rate
        sampleRate = 44100

        print('opening stream...')
        stream = p.open(format = pyaudio.paInt16,
                        channels = 1,
                        rate = sampleRate,
                        input = True,
                        frames_per_buffer = fftLen,
                        input_device_index = inputIndex)
        try:
            while True:
                # read a chunk of data
                data = stream.read(fftLen)
                # convert to numpy array
                dataArray = numpy.frombuffer(data, dtype=numpy.int16)
                # get FFT of data
                fftVals = numpy.fft.rfft(dataArray)*2.0/fftLen
                # get absolute values of complex numbers
                fftVals = numpy.abs(fftVals)
                # average 3 frequency bands: 0-100 Hz, 100-1000 Hz and 1000-2500 Hz
                levels = [numpy.sum(fftVals[0:100])/100,
                          numpy.sum(fftVals[100:1000])/900,
                          numpy.sum(fftVals[1000:2500])/1500]

                # the data sent is of the form:
                # 'H' (header), speed1, dir1, speed2, dir2
                vals = [ord('H'), 100, 1, 100, 1]

                # speed1
                vals[1] = int(5*levels[0]) % 255
                # speed2
                vals[3] = int(100 + levels[1]) % 255

                # dir
                d1 = 0
                if levels[2] > 0.1:
                    d1 = 1
                vals[2] = d1
                vals[4] = 0

                # pack data
                data = struct.pack('BBBBB', *vals)
                # write data to serial port
                ser.write(data)
                # a slight pause
                sleep(0.001)
        except KeyboardInterrupt:
            print('stopping...')
        finally:
            print('cleaning up')
            stream.close()
            p.terminate()
            # shut off motors
            vals = [ord('H'), 0, 1, 0, 1]
            data = struct.pack('BBBBB', *vals)
            ser.write(data)
            # close serial
            ser.flush()
            ser.close()

# main method
def main():
    # parse arguments
    parser = argparse.ArgumentParser(description='Analyzes audio input and
sends motor control information via serial port')
    # add arguments
    parser.add_argument('--port', dest='serial_port_name', required=True)
    parser.add_argument('--mtest', action='store_true', default=False)
    parser.add_argument('--atest', action='store_true', default=False)
    args = parser.parse_args()

    # open serial port
    strPort = args.serial_port_name
    print('opening ', strPort)
    ser = serial.Serial(strPort, 9600)
    if args.mtest:
        manualTest(ser)
    elif args.atest:
        autoTest(ser)
    else:
        fftLive(ser)

# call main function
if __name__ == '__main__':
    main()

Running the Program

To test the project, assemble the hardware, connect the Arduino to the computer, and upload the motor driver code into the Arduino. Make sure the battery pack is connected and that your laser pointer is on and projecting on a flat surface like a wall. I recommend testing the laser display part first by running the following program. (Don’t forget to change the serial port string to match your computer!)

python3 laser.py --port /dev/tty.usbmodem411 --atest
('opening ', '/dev/tty.usbmodem1411')
staring automatic test...
[25, 0, 25, 0]
[35, 0, 25, 0]
[45, 0, 25, 0]
...

This test runs both motors through various combinations of speeds and direction. You should see different laser patterns projected onto your wall. To stop the program and the motors, press CTRL-C.

If the test succeeds, you’re ready to move on to the real show. Start playing your favorite music on your computer and run the program as follows. (Again, watch that serial port string!)

python3 laser.py --port /dev/tty.usbmodem411
('opening ', '/dev/tty.usbmodem1411')
Found 4 devices. Select input device:
0: Built-in Microph
1: Built-in Output
2: BoomDevice
3: AirParrot
0
Input device chosen: Built-in Microph
opening stream...

You should see the laser display produce lots of interesting patterns that change in time with the music, as shown in Figure 13-10.

Image

Figure 13-10: The complete wiring of the laser display and a pattern projected on the wall

Summary

In this chapter, you upped your Python and Arduino skills by building a more complex project. You learned how to control motors with Python and Arduino, and used numpy to get the FFT of audio data, serial communications, and even lasers!

Experiments!

Here are some ways you can modify this project:

1. The program used an arbitrary scheme to convert the FFT values into motor speed and direction data. Try changing this scheme. For example, experiment with different frequency bands and criteria for changing motor directions.

2. In this project, you converted frequency information gathered from the audio signal to motor speed and direction. Try making the motors move according to the overall “pulse” or volume of the music. For this, you can compute the root mean square (RMS) value of the amplitude of the signal. This computation is similar to the FFT calculation. Once you read in a chunk of audio data and put it into a numpy array x, you can compute the RMS value as follows:

rms = numpy.sqrt(numpy.mean(x**2))

Also, remember that the amplitude in your project was expressed as a 16-bit signed integer, which can have a maximum value of 32,768 (a useful number to keep in mind for normalization). Use this RMS amplitude in conjunction with the FFT to generate a greater variation of laser patterns.

3. In the project, you rather crudely used some tape to keep the laser pointer on to test and run your hardware setup. Can you find a better way to control the laser? Read up about optoisolators and relays,1 which are devices you can use to switch external circuits on and off. To use these, you first need to hack your laser pointer so it can be toggled by an external switch. One way to do so is to glue the button of the laser pointer to the ON position permanently, remove the batteries, and solder two leads onto the battery contacts. Now you can switch the laser pointer on and off manually using these wires and the laser pointer’s batteries. Next, replace this scheme with a digital switch by wiring the laser pointer through a relay or optoisolator and switching it on using a digital pin on your Arduino. If you use an optoisolator, you can toggle the laser on and off directly with the Arduino. If you’re using a relay, you will also need a driver, usually in the form of a simple transistorbased circuit.

Once you have this set up, add some code so that when the Python program runs, a serial command is sent to the Arduino to switch on the laser pointer before the show starts.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset