Acoustics

Enter Your Electronics & Design Project for a chance to win an $200 Shopping cart of product!

Back to The Project14 homepage

Project14 Home
Monthly Themes
Monthly Theme Poll

 

After moving and lighting stuff for months, I moved to some, hopefully nice, acoustic/audio project. In this context, the project I am presenting, as well as the next projects I have in mind, are more acoustic projects than pure audio builds.

This project has been published for the first time on The Shed Magazine issues 88 and 89

Before seeing the project details, take a look at the video below, the making-of and testing of the Pi Rotary!

 

 

Pi Rotary is based on the upcycling of a rotary phone from the end of 60s' using a Raspberry Pi to create a Pi-rotary smartphone.

A part of the device I decided to exclude is the phone ring bell: controlling an old phone ring bell with digital low voltage signals may be more complex than expected as the bell rings working at a relatively high voltage. Together with the electrical issue, I had to find a way to create the space for the new components and using another class of audio messages – as we will see later – the ring bell results useless.

To provide audio features to the device I disassembled a cheap USB and Bluetooth portable amplifier with speakers. the device includes a 3.5 mm Jack audio input for wired connection, used to amplify the Raspberry Pi audio output.

 

Above: the small amplifier board with the LiIon rechargeable battery and the two stereo speakers.

 

The amplifier is powered by a temporary switch button. After powering on, the default setting is Bluetooth mode; to switch to wired mode another temporary switch button should be pressed once. The power sequence need the button pressed for about five seconds (until the amplified emits a sound); the same when switching from Bluetooth mode for about half a second.

 

Above: the amplifier board. The button near the 3.5mm audio jack is the power button while the other temporary switch to the opposite side is the mode button

 

Pressing the power button for three or five seconds (mine needs five), the device powers on emitting a sound, automatically setting the Bluetooth mode. With the help of a handy multimeter I found the points where the buttons close the circuit and I was able to reproduce the buttons behaviors. Soldering a couple of wires in correspondence to the contacts of the temporary switch these can be controlled digitally. Joining the two end wires of one of the buttons we can expect to obtain the same effect as by pressing it but to control them by the Raspberry Pi needs some electronics.

 

Above: the control wires soldered to the corresponding temporary switches on the amplifier board.

 

As a matter of fact, when we change the state of a pin of the Raspberry GPIO, we just provide a 3.3V current level, not physically closing a circuit for which we need the equivalent of a relais. The solution is not so complex using a common NPN transistor (I used a PN2222A) for every button: the two soldered wires coming from a button will be connected to the collector and the emitter of the transistor that is normally open (same as the button in its normal state). Sending a digital signal “high” from a GPIO pin to the base of the transistor – and so powering the transistor itself – the transistor closes the circuit and we get the button pressed.

 

Above: the two buttons wired on the prototype PCB board with the two transistors. The third image shows the button connection circuit.

 

The easiest phone component to connect is the microphone hang-on switch. After removing the ring bell and the solenoid group, I completely disassembled the phone in order to remove the electronic circuit screwed onto the base of the phone case; then, I desoldered the components leaving only the PCB and the hang-on switch.

 

{gallery} Disassembling the Rotary Phone

The rotary phone fully disassembled

The hang-up switch on the PCB control circuit of the phone

Preparing the phone base to host the new components

The main PCB with the components removed and the hang-up wires soldered on the clean PCB

 

A challenging part of this project has been the rotary dial control. The dialer has two wires that close a contact when the rotary wheel is rotated clockwise; the contact between these two wires remains closed until the rotary wheel is not released and returns to its original position rotating counterclockwise. Reading from a Raspberry Pi GPIO pin the status of this contact we can know when the user starts dialing a number.

 

Above: the back-side of the rotary dialer with the two couple of wires.

 

The other two wires, instead, close and open the contact multiple times when the rotary wheel is released to compose a number generating a series of pulses accordingly with the number: the cipher "0" corresponds to ten pulses, “1” corresponds to one pulse and so on. The number of pulses can be easily read from a Raspberry Pi GPIO input pin.

 

Above: the full circuit to control the telephone and the amplifier buttons through the Raspberry Pi GPIO.

 

The reading circuit on the hangout switch is the same used reading a common temporary switch button. To avoid multiple readings when the contacts are closed, I have added a 0.1 uF capacitor between the two wires on every terminal of the rotary dial, a debouncing component smoothing the read persistence signal for some milliseconds and avoid false-positive readings.

The circuit connected on the Raspberry Pi GPIO to control the amplifier buttons worked fine, as well as the three LEDs used to provide some essential visual notification to the user.

 

Above: the breadboard test circuit during the design phase, before making the PCB

 

Adapting the circuit and the components to the internal design of the phone to host the Raspberry Pi (with the circuit), the amplifier and the batteries was a challenging task; at least it is what I thought until I  faced the real challenge making all the things working together. I decided to make this project battery-operated because the circuit of the amplifier was already equipped with a LiIon USB rechargeable battery; to power the Raspberry I decided to add a Pi Juice board to run on battery the Raspberry Pi in the most efficient way.

 

Above: the Raspberry Pi with the Pi Juice HAT

 

After testing all the components controlled by the Raspberry Pi on the breadboard, using a Pi-sized PCB breadboard I made the final small circuit board fit on the pass-through 40-pins connectors of the Pi exposed by the Pi Juice HAT.

 

Above: the Raspberry Pi custom circuit to control all the parts of the rotary phone created on a Pi-sized breadboard PCB.

 

The circuit part to manage the rotary dial pulses is almost easy. If you search on Google you will find plenty of working examples on how to count the pulses generated by the rotary dial and convert them into the corresponding numbers. Unfortunately, the projects and documentation, as well as the source code, to manage this task are based on the Arduino board, and things become more and more complex when trying to accomplish it on a Linux machine like the Raspberry Pi.

Arduino is a microcontroller running continuously a program and as the dialer signal goes high, it starts counting the pulses generated by the rotary dial. The generated pulses can be detected connecting a pin from the board GPIO to one of the two wires of the rotary dialer while the other wire is connected to the power line: when the signal goes high, a pulse is generated for the duration of about 10 milliseconds. The circuitry to reach the same result on the Raspberry Pi using Python is the same as on Arduino. The issue lays in the Linux operating system behavior; when an event happens, like  a pulse is received from a GPIO pin on the Raspberry Pi, it is not certain that the operating system reserves all the resources to this specific task in a timely manner. At least not in the normal way we can expect using the GPIO library for Python and the Python source. This issue turned me crazy for a week as, regardless of the approach, I was always missing detecting some pulses. The correspondence between the number of pulses and the dialed number is direct:

 

1 = one pulse, 2 = two pulses, 3 = three pulses and so on up to 0 = ten pulses.

 

The rotary dial is essential in this project as it is the main interface between the user and the device. The problem was partially solved with hardware interrupts in the Python source. The first resulting program was a full state machine that waited indefinitely for a hardware event detected by the GPIO configured pins. Every numeric command is represented by a three-ciphers sequence parsed when the third is dialed.

The list below shows the commands available spoke by the Pi Rotary then the number '111' is dialed.

 

  •     "Wait for the red light before dialing a command",
  •     "Dial 1 2 3: play all the playlist in order",
  •     "Dial 1 2 4: list all the playlist titles",
  •     "Dial 3 2 1: play the next track in the playlist",
  •     "Dial numbers from 4 0 1 to play the corresponding track in the playlist",
  •     "Dial 6 6 6: hot reset the Pi Rotary",
  •     "Dial 9 9 9: cold reset the Pi Rotary",
  •     "Dial 1 0 0: hear last weather report",
  •     "Dial 1 1 1: these help notes"

 

Note that all the textual information, as well as the messages and song tracks titles are defined in the two Json files 'playlist.json' and 'comments.json'

 

if __name__ == '__main__':
    # Main application
    initGPIO()
    
    # looping infinitely
    while True:
        pass

 

Besides, as shown in the main program of the PiRotary.py program, when working as a pure state machine there were still cases when the program was not able to process correctly the pulses count. What I needed was a GPIO reading engine in Python able to take control of the main process reading all the events during the dial event.

 

Above: the typical pulse train generated when a number is dialed.

 

To make things more complex, the rotary dial pulse generator is an analog device; as shown in the above image, the pulse train can have small differences between pulses so sampling the state of the pulse connector at a specified interval can't work fine.

What I needed was a Python library interfacing the GPIO hardware events in a more accurate way; after trying several GPIO libraries for Python, finally I adopted the PiGPIO library (http://abyz.me.uk/rpi/pigpio/python.html) that fits perfectly to solve tasks like this. As a matter of fact this multi-platform library is a C component that can be compiled in the Windows, Mac, and Linux environments, including the Raspberry Pi Linux operating system. The advantage of the library is that it uses a 'daemon', a program running at a lower level than the other common processes like the Python interpreter and can read any GPIO pin status change at a high frequency. An interface available for Python makes it easy to include the features inside the application.

 

Above: screenshot of the Raspberry Pi desktop. The left window shows a perfect reading of the rotary dial pulse train via the PiGPIO library in Python using the daemon socket.

 

A second important feature I have implemented in the Pi Rotary is the ability to answer the user with natural language sentences; the program can also be localized easily, simply changing the language of the text strings in the json files. To make the device able to speak the sentences I used 'trans', a terminal shell script that accepts a text string as input and plays the generated audio file using the Google Translate service.

The mp3 player instead uses the terminal command 'mplayer,' while the mp3 tracks are stored in the Music folder of the system.

The Python function check_number() below shows how the commands are processed by the parser.

 

def check_number():
    '''
    Check if the current number corresponds to a valid command.
    '''
    global dialed_number
    global track_position
    global pi

    debug_message(str(dialed_number))

    # Numeric commands and related functions
    if dialed_number is not '':
        if int(dialed_number) == 666:
            # Restart the appplication to initial conditions
            dialed_number = ''
            reinit()

        # Play the next track
        elif int(dialed_number) == 321:
            # Disable the interrupts until finished
            release_callbacks()
            # Start playing the next track
            play_track()
            # Enable the interrupts
            cb_set_hangout()
            # If the hangout is still active, enable the dialer too
            if pi.read(pin_hangout_led) is PI_HIGH:
                cb_set_rotary()

        # Play all tracks in sequence
        elif int(dialed_number) == 123:
            # Disable the interrupts until finished
            release_callbacks()
            # Play the entire playlist
            play_all_tracks()
            # Enable the interrupts
            cb_set_hangout()
            # If the hangout is still active, enable the dialer too
            if pi.read(pin_hangout_led) is PI_HIGH:
                cb_set_rotary()


        # Tell the playlist titles
        elif int(dialed_number) == 124:
            # Disable the interrupts until finished
            release_callbacks()
            # Tell the playlist titles
            list_all_tracks()
            # Enable the interrupts
            cb_set_hangout()
            # If the hangout is still active, enable the dialer too
            if pi.read(pin_hangout_led) is PI_HIGH:
                cb_set_rotary()


        # Play the desired track
        elif (int(dialed_number) <= tracks + 400) and (int(dialed_number) > 400):
            track_position = int(dialed_number) - 401
            # Disable the interrupts until finished
            release_callbacks()
            # Start playing the next track
            play_track()
            # Enable the interrupts
            cb_set_hangout()
            # If the hangout is still active, enable the dialer too
            if pi.read(pin_hangout_led) is PI_HIGH:
                cb_set_rotary()


        # Tell the help notes
        elif int(dialed_number) == 111:
            # Disable the interrupts until finished
            release_callbacks()
            # Tell the help messages
            say_help()
            # Enable the interrupts
            cb_set_hangout()
            # If the hangout is still active, enable the dialer too
            if pi.read(pin_hangout_led) is PI_HIGH:
                cb_set_rotary()


        elif int(dialed_number) == 999:
            # Reboot the system
            runCmd([REBOOT[0], REBOOT[1], REBOOT[2]])


        # Tell the weather
        elif int(dialed_number) == 100:
            # Disable the interrupts until finished
            release_callbacks()
            # Retrieve and say the weather message
            get_weather()
            # Enable the interrupts
            cb_set_hangout()
            # If the hangout is still active, enable the dialer too
            if pi.read(pin_hangout_led) is PI_HIGH:
                cb_set_rotary()


        elif int(dialed_number) == 999:
            # Reboot the system
            runCmd([REBOOT[0], REBOOT[1], REBOOT[2]])

 

Once a dialed number has been recognized as the corresponding command, the program stops the interrupts in order to avoid any interferences until the command execution has not been completed.

I had to adopt this solution, including the use of the red LED as a ready signal when the program is again able to accept a new command, because also using the PiGPIO library I experienced some problems when the program starts other tasks like the mp3 player or while saying a sentence. The reason is that the audio output is generated by the player by streaming the data that should not be interrupted to avoid getting a bad audio sound. The program worked perfectly after applying this logic. The limit of the program is that it is not possible to accept any command while a task is in execution.

A last upgrade to the program before considering the project closed was the addition of the command '100'. Using the Linux terminal command 'weather' after installing the Weather Utilities (https://www.maketecheasier.com/weather-forecasts-raspberry-pi/) I got the command available to be called as an external process from the Python program.

 

def get_weather():
    '''
    Retrieve the weather from the nearest airport. The desired international
    airport weather station ICAO four-character code should be set in the
    comments.json file.
    The command speaks the weather data returned from the call.
    '''
    global is_playing
    global weather_airport
    global weather_ICAO


    # Enable the amplifier and set the playing flag
    is_playing = True
    ampli_on_off()


    # Announce the weather retrieval
    runCmd([TTS[0], TTS[1], weather_airport])


    # Execute the weather command
    cmd = [WEATHER[0], weather_ICAO]


    proc = subprocess.Popen(cmd,
                            stdout=subprocess.PIPE,
                            stderr=subprocess.PIPE,
                            )
    stdout, stderr = proc.communicate()
    # Divide the weather message in a list of single lines
    # removing the newline characters
    forecast = stdout.splitlines()


    w = 4   # First useful line of the weather forecast


    # Say the forecast meaningful strings (starting from 4)
    # Note that forecast is a list of bytes so every text line
    # should be decoded to the corresponding ASCII string
    while w < len(forecast):
        text = forecast[w].decode('ascii')
        runCmd([TTS[0], TTS[1], text])
        w += 1


    # Disable the amplifier, if it has not yet disabled by the user
    if is_playing is True:
        ampli_on_off()
        is_playing = False

 

The above function get_weather() processes the weather forecast information provided by the terminal command 'weather'. Also, the forecast data are audio spoken, but the data generated by the command are not suitable to be spoken as they are. The function extracts from the generated string of the command, as shown in the example below, a series of sentences good for text-to-speech.

Below, an example of the weather command forecast:

 

Last updated Feb 29, 2020 - 11:20 AM EST / 2020.02.29 1620 UTC
Temperature: 46 F (8 C)
Relative Humidity: 65%
Wind: from the WSW (240 degrees) at 18 MPH (16 KT)
Sky conditions: mostly clear

 

The command weather can be configured in several ways, but to get the fastest response you have to specify the ICAO four-characters airport code. ICAO – International Civil Aviation Organization – (source: Wikipedia)

Using this method, it is possible to localize the weather forecast command depending on the desired location.

The sources and Python library of the project, as well as the STL files for the LED covers and the circuit design, are available under Open Source license on the GitHub repository https://github.com/alicemirror/PiRotary