* feat: add installer script, revise methodology

* feat: update installer.sh script to fix issues

* fix: re-name service to .template

* fix: line endings, script update, guestbook output_file mod

* fix: lambda revision

* fix: add NC/NO logic for hook_type
This commit is contained in:
Nick Pourazima 2023-10-09 14:11:45 -06:00 committed by GitHub
parent 3b5e32f781
commit 3eddb1c300
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 327 additions and 181 deletions

3
.gitignore vendored
View File

@ -1 +1,2 @@
*.code* *.code*
*.trunk*

142
README.md
View File

@ -12,13 +12,18 @@
- [Microphone Replacement (Optional)](#microphone-replacement-optional) - [Microphone Replacement (Optional)](#microphone-replacement-optional)
- [Software](#software) - [Software](#software)
- [Dev Environment](#dev-environment) - [Dev Environment](#dev-environment)
- [Dependencies](#dependencies) - [Installation](#installation)
- [audioGuestBook systemctl service](#audioguestbook-systemctl-service)
- [Config](#config) - [Config](#config)
- [AudioInterface Class](#audiointerface-class) - [AudioInterface Class](#audiointerface-class)
- [audioGuestBook systemctl service](#audioguestbook-systemctl-service)
- [Operation Mode 1: audioGuestBook](#operation-mode-1-audioguestbook) - [Operation Mode 1: audioGuestBook](#operation-mode-1-audioguestbook)
- [Operation Mode 2: audioGuestBookwithRotaryDialer](#operation-mode-2-audioguestbookwithrotarydialer) - [Operation Mode 2: audioGuestBookwithRotaryDialer](#operation-mode-2-audioguestbookwithrotarydialer)
- [Troubleshooting](#troubleshooting) - [Troubleshooting](#troubleshooting)
- [Configuring Hook Type](#configuring-hook-type)
- [Verify default audio interface](#verify-default-audio-interface)
- [Check the Sound Card Configuration](#check-the-sound-card-configuration)
- [Set the Default Sound Card](#set-the-default-sound-card)
- [Restart ALSA](#restart-alsa)
- [Support](#support) - [Support](#support)
This project transforms a rotary phone into a voice recorder for use at special events (i.e. wedding audio guestbook, etc.). This project transforms a rotary phone into a voice recorder for use at special events (i.e. wedding audio guestbook, etc.).
@ -37,29 +42,29 @@ Since this was a trial by fire type of scenario there ended up being a few gotch
### Future Work (Action Items) ### Future Work (Action Items)
A few weeks before the wedding I had the code registering dialed numbers from the rotary encoder with the goal of playing back special messages for certain guests who dialed a certain combination (i.e. dial an area code to hear a special message to my old roomates). The details of this operation mode are described in [Mode 2](#operation-mode-2-rotaryguestbookwithrotarydialer) below. In order to activate this mode I had to wait for input when the phone was off the hook. This required an extra step of dialing zero before leaving a normal voice message. In the end we decided to keep it simple and I've thus migrated this code to the dev branch along with the code to run through post-porcessing the audio in a separate process. A few weeks before the wedding I had the code registering dialed numbers from the rotary encoder with the goal of playing back special messages for certain guests who dialed a certain combination (i.e. dial an area code to hear a special message to my old roomates). The details of this operation mode are described in [Mode 2](#operation-mode-2-audioguestbookwithrotarydialer) below. In order to activate this mode I had to wait for input when the phone was off the hook. This required an extra step of dialing zero before leaving a normal voice message. In the end we decided to keep it simple and I've thus migrated this code to the dev branch along with the code to run through post-porcessing the audio in a separate process.
If any one is interested in expanding this please feel free. If any one is interested in expanding this please feel free.
I would also like to thread the audio playback so I can have a monitor/watchdog service terminate the thread upon hook callback so that the message doesn't continue playing once the user hangs up. I would also like to thread the audio playback so I can have a monitor/watchdog service terminate the thread upon hook callback so that the message doesn't continue playing once the user hangs up.
## Materials ## Materials
| Part|Notes|Quantity|Cost| | Part | Notes | Quantity | Cost |
| - | - | - | - | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------ |
| [rotary phone](https://www.ebay.com/b/Rotary-Dial-Telephone/38038/bn_55192308) | Estate/garage/yard sales are probably the best places to find once of these. Ideally one with a phone jack since we will be using these four wires extensively. | 1 | $0.00-$60.00 | | [rotary phone](https://www.ebay.com/b/Rotary-Dial-Telephone/38038/bn_55192308) | Estate/garage/yard sales are probably the best places to find once of these. Ideally one with a phone jack since we will be using these four wires extensively. | 1 | $0.00-$60.00 |
| [raspberry pi zero](https://www.raspberrypi.com/products/raspberry-pi-zero/) | I didn't realize how hard these are to find these days. You can use any rpi or arduino style single-board computer but be aware of size constraints (i.e. must fit inside the rotary phone enclosure) | 1 | $9.99 | | [raspberry pi zero](https://www.raspberrypi.com/products/raspberry-pi-zero/) | I didn't realize how hard these are to find these days. You can use any rpi or arduino style single-board computer but be aware of size constraints (i.e. must fit inside the rotary phone enclosure) | 1 | $9.99 |
| [raspberry pi zero case](https://www.adafruit.com/product/3252) | Optional: added for protection. One of the cases on Amazon has a heat-sink cutout which might be nice for better heat dissapation since it will all be enclosed in the end. | 1 | $4.95 | | [raspberry pi zero case](https://www.adafruit.com/product/3252) | Optional: added for protection. One of the cases on Amazon has a heat-sink cutout which might be nice for better heat dissapation since it will all be enclosed in the end. | 1 | $4.95 |
| [micro SD card](https://a.co/d/1gb2zhC) | Any high capacity/throughput micro SD card that is rpi compatible | 1 | $8.99 | | [micro SD card](https://a.co/d/1gb2zhC) | Any high capacity/throughput micro SD card that is rpi compatible | 1 | $8.99 |
| [USB Audio Adapter](https://www.adafruit.com/product/1475) | Note: I removed the external plastic shell and directly soldered the wires instead of using the female 3.5mm receptacle. | 1 | $4.95 | | [USB Audio Adapter](https://www.adafruit.com/product/1475) | Note: I removed the external plastic shell and directly soldered the wires instead of using the female 3.5mm receptacle. | 1 | $4.95 |
| [USB OTG Host Cable - MicroB OTG male to A female](https://www.adafruit.com/product/1099) | | 1 | $2.50 | | [USB OTG Host Cable - MicroB OTG male to A female](https://www.adafruit.com/product/1099) | | 1 | $2.50 |
| --- | **--- If you don't want to solder anything ---** | --- | --- | | --- | **--- If you don't want to solder anything ---** | --- | --- |
| [3.5mm Male to Screw Terminal Connector](https://www.parts-express.com/3.5mm-Male-to-Screw-Terminal-Connector-090-110?quantity=1&utm_source=google&utm_medium=cpc&utm_campaign=18395892906&utm_content=145242146127&gadid=623430178298&gclid=CjwKCAiAioifBhAXEiwApzCztl7aVb18WP4hDxnlQUCHsb62oIcnduFCSCbn9LFkZovYTQdr6omb3RoCD_gQAvD_BwE) | Optional: can connect the handset cables directly to the USB audio interface via these screw terminals | 2 | $1.37 | | [3.5mm Male to Screw Terminal Connector](https://www.parts-express.com/3.5mm-Male-to-Screw-Terminal-Connector-090-110?quantity=1&utm_source=google&utm_medium=cpc&utm_campaign=18395892906&utm_content=145242146127&gadid=623430178298&gclid=CjwKCAiAioifBhAXEiwApzCztl7aVb18WP4hDxnlQUCHsb62oIcnduFCSCbn9LFkZovYTQdr6omb3RoCD_gQAvD_BwE) | Optional: can connect the handset cables directly to the USB audio interface via these screw terminals | 2 | $1.37 |
| --- | **--- If running off a battery ---** | --- | --- | | --- | **--- If running off a battery ---** | --- | --- |
| [LiPo Battery](https://www.adafruit.com/product/2011)| Optional: maximize capacity based on what will fit within your rotary enclosure. |1| $12.50 | | [LiPo Battery](https://www.adafruit.com/product/2011) | Optional: maximize capacity based on what will fit within your rotary enclosure. | 1 | $12.50 |
| [LiPo Shim](https://www.adafruit.com/product/3196)| Optional: if you plan to run this off a LiPo I would recommend something like this to interface with the rpi zero. |1| $9.95 | | [LiPo Shim](https://www.adafruit.com/product/3196) | Optional: if you plan to run this off a LiPo I would recommend something like this to interface with the rpi zero. | 1 | $9.95 |
| [LiPo Charger](https://www.adafruit.com/product/1904) | Optional: for re-charging the LiPo. |1| $6.95 | | [LiPo Charger](https://www.adafruit.com/product/1904) | Optional: for re-charging the LiPo. | 1 | $6.95 |
| --- | **--- If replacing the built-it microphone ---** | --- | --- | | --- | **--- If replacing the built-it microphone ---** | --- | --- |
| [LavMic](https://www.amazon.com/dp/B01N6P80OQ?ref=nb_sb_ss_w_as-reorder-t1_ypp_rep_k3_1_9&amp=&crid=15WZEWMZ17EM9&amp=&sprefix=saramonic) | Optional: if you'd like to replace the carbon microphone. This is an omnidirectional lavalier mic and outputs via a 3.5mm TRS | 1 | $24.95 | | [LavMic](https://www.amazon.com/dp/B01N6P80OQ?ref=nb_sb_ss_w_as-reorder-t1_ypp_rep_k3_1_9&amp=&crid=15WZEWMZ17EM9&amp=&sprefix=saramonic) | Optional: if you'd like to replace the carbon microphone. This is an omnidirectional lavalier mic and outputs via a 3.5mm TRS | 1 | $24.95 |
## Hardware ## Hardware
@ -67,28 +72,35 @@ I would also like to thread the audio playback so I can have a monitor/watchdog
#### Hook #### Hook
**Understanding Hook Types:** Depending on your rotary phone model, the hook switch may be Normally Closed (NC) or Normally Open (NO). When the phone is on the hook:
- NC: The circuit is closed (current flows).
- NO: The circuit is open (no current).
To accommodate either type, you'll need to update the `config.yaml` with the appropriate hook type setting.
- Use multimeter to do a continuity check to find out which pins control the hook: - Use multimeter to do a continuity check to find out which pins control the hook:
| On-hook --> Open circuit (Value == 1) | Off-hook --> Current flowing | | On-hook --> Open circuit (Value == 1) | Off-hook --> Current flowing |
| ------------- | ------------- | | ------------------------------------- | -------------------------------- |
| ![image](images/hook_test_1.jpg) | ![image](images/hook_test_2.jpg) | | ![image](images/hook_test_1.jpg) | ![image](images/hook_test_2.jpg) |
- The B screw terminal on the rotary phone is connected to the black wire which is grounded to the rpi. - The B screw terminal on the rotary phone is connected to the black wire which is grounded to the rpi.
- The L2 screw terminal on the rotary phone is connected to the white wire which is connected to GPIO pin 22 on the rpi. - The L2 screw terminal on the rotary phone is connected to the white wire which is connected to GPIO pin 22 on the rpi.
![image](images/pi_block_terminal_wiring.jpg) ![image](images/pi_block_terminal_wiring.jpg)
- *Note: the green wire was used for the experimental rotary encoder feature identified in the [future work](#future-work-action-items) section.* - _Note: the green wire was used for the experimental rotary encoder feature identified in the [future work](#future-work-action-items) section._
| Rotary Phone Block Terminal | Top-down view |
| ----------------------------------- | -------------------------------------------- |
| ![image](images/block_terminal.jpg) | ![image](images/top_view_block_terminal.jpg) |
| Rotary Phone Block Terminal | Top-down view |
| ------------- | ------------- |
| ![image](images/block_terminal.jpg) | ![image](images/top_view_block_terminal.jpg) |
#### Phone Cord #### Phone Cord
- The wires from the handset cord need to be connected to the USB audio interface - The wires from the handset cord need to be connected to the USB audio interface
- I soldered it but you can alternatively use 2x [3.5mm Male to Screw Terminal Connector](https://www.parts-express.com/3.5mm-Male-to-Screw-Terminal-Connector-090-110?quantity=1&utm_source=google&utm_medium=cpc&utm_campaign=18395892906&utm_content=145242146127&gadid=623430178298&gclid=CjwKCAiAioifBhAXEiwApzCztl7aVb18WP4hDxnlQUCHsb62oIcnduFCSCbn9LFkZovYTQdr6omb3RoCD_gQAvD_BwE) which plug directly into the rpi. - I soldered it but you can alternatively use 2x [3.5mm Male to Screw Terminal Connector](https://www.parts-express.com/3.5mm-Male-to-Screw-Terminal-Connector-090-110?quantity=1&utm_source=google&utm_medium=cpc&utm_campaign=18395892906&utm_content=145242146127&gadid=623430178298&gclid=CjwKCAiAioifBhAXEiwApzCztl7aVb18WP4hDxnlQUCHsb62oIcnduFCSCbn9LFkZovYTQdr6omb3RoCD_gQAvD_BwE) which plug directly into the rpi.
- *Note: The USB audio interface looks weird in the pics since I stripped the plastic shell off in order to solder directly to the mic/speaker leads* - _Note: The USB audio interface looks weird in the pics since I stripped the plastic shell off in order to solder directly to the mic/speaker leads_
![image](images/dissected_view_1.jpg) ![image](images/dissected_view_1.jpg)
@ -132,13 +144,36 @@ To replace:
[Here's](https://jayproulx.medium.com/headless-raspberry-pi-zero-w-setup-with-ssh-and-wi-fi-8ddd8c4d2742) a great guide to get the rpi setup headless w/ SSH & WiFi dialed in. [Here's](https://jayproulx.medium.com/headless-raspberry-pi-zero-w-setup-with-ssh-and-wi-fi-8ddd8c4d2742) a great guide to get the rpi setup headless w/ SSH & WiFi dialed in.
### Dependencies ### Installation
- `pip3 install -r requirements.txt` or pip install each manually: After cloning the repository, there's an installer script available to ease the setup process. This script takes care of several tasks:
- [GPIOZero](https://gpiozero.readthedocs.io)
- [Pydub](http://pydub.com/) 1. Install required dependencies.
- [PyAudio](https://people.csail.mit.edu/hubert/pyaudio/) 2. Replace placeholders in the service file to adapt to your project directory.
- [PyYAML](https://pyyaml.org/) 3. Move the modified service file to the systemd directory.
4. Create necessary directories (recordings and sounds).
5. Grant execution permissions to the Python scripts.
6. Reload systemd, enable, and start the service.
To run the installer, navigate to the project directory and execute:
```bash
chmod +x installer.sh
./installer.sh
```
### [audioGuestBook systemctl service](audioGuestBook.service)
The provided service ensures the Python script starts on boot. The installer script will place this service file in the `/etc/systemd/system`` directory and modify paths according to your project directory.
To manually control the service:
```sh
sudo systemctl enable audioGuestBook.service
sudo systemctl start audioGuestBook.service
```
This service ensures smooth operation without manual intervention every time your Raspberry Pi boots up.
### [Config](config.yaml) ### [Config](config.yaml)
@ -146,39 +181,18 @@ To replace:
- Ensure the sample rate is supported by your audio interface (default = 44100 Hz (decimal not required)) - Ensure the sample rate is supported by your audio interface (default = 44100 Hz (decimal not required))
- For GPIO mapping, refer to the wiring diagram specific to your rpi: - For GPIO mapping, refer to the wiring diagram specific to your rpi:
![image](images/rpi_GPIO.png) ![image](images/rpi_GPIO.png)
- **hook_type**: Define your hook switch type here. Set it to "NC" if your phone uses a Normally Closed hook switch or "NO" for Normally Open.
### [AudioInterface Class](audioInterface.py) ### [AudioInterface Class](audioInterface.py)
- Utilizes pydub and pyaudio extensively. - Utilizes pydub and pyaudio extensively.
- Houses the main playback/record logic and has future #TODO expansion for postprocessing the audio. Would like to test on an rpi4 to see if it can handle it better for real-time applications. - Houses the main playback/record logic and has future #TODO expansion for postprocessing the audio. Would like to test on an rpi4 to see if it can handle it better for real-time applications.
#### [audioGuestBook systemctl service](/audioGuestBook.service)
This service starts the python script on boot. Place it in the `/etc/systemd/system` directory and modify the **WorkingDirectoy** and **ExecStart** paths below according to the specific directory in which you cloned the project, i.e.:
```sh
[Unit]
Description=Rotary Phone Guest Book Project
After=multi-user.target
[Service]
WorkingDirectory=/home/<username>/Desktop/rotaryGuestBook
User=<username>
Type=simple
Restart=always
ExecStart=/usr/bin/env python3 /home/<username>/Desktop/rotaryGuestBook/rotaryGuestBook.py
[Install]
WantedBy=multi-user.target
```
```sh
systemctl enable audioGuestBook.service
systemctl start audioGuestBook.service
```
### Operation Mode 1: [audioGuestBook](/audioGuestBook.py) ### Operation Mode 1: [audioGuestBook](/audioGuestBook.py)
- This is the main operation mode of the device. - This is the main operation mode of the device.
- There are two callbacks in main which poll the gpio pins for the specified activity (hook depressed, hook released). - There are two callbacks in main which poll the gpio pins for the specified activity (hook depressed, hook released).
- In the code, depending on the `hook_type` set in the `config.yaml`, the software will adapt its behavior. For NC types, hanging up the phone will trigger the `on_hook` behavior, and lifting the phone will trigger the `off_hook` behavior. The opposite will be true for NO types.
- Once triggered the appropriate function is called. - Once triggered the appropriate function is called.
- On hook (depressed) - On hook (depressed)
- Nothing happens - Nothing happens
@ -189,7 +203,7 @@ systemctl start audioGuestBook.service
### Operation Mode 2: [audioGuestBookwithRotaryDialer](./todo/audioGuestBookwithRotaryDialer.py) ### Operation Mode 2: [audioGuestBookwithRotaryDialer](./todo/audioGuestBookwithRotaryDialer.py)
***Note*:** Untested - decided not to go this route for my own wedding **_Note_:** Untested - decided not to go this route for my own wedding
- This mode is a special modification of the normal operation and requires a slightly different wiring connection since it accepts input from the rotary dialer. - This mode is a special modification of the normal operation and requires a slightly different wiring connection since it accepts input from the rotary dialer.
- The idea was to playback special messages when particular users dial a certain number combination (i.e. 909 would play back a message for certain guests who lived with the groom in that area code). - The idea was to playback special messages when particular users dial a certain number combination (i.e. 909 would play back a message for certain guests who lived with the groom in that area code).
@ -198,11 +212,15 @@ systemctl start audioGuestBook.service
## Troubleshooting ## Troubleshooting
### Configuring Hook Type
If you find that the behaviors for hanging up and lifting the phone are reversed, it's likely that the `hook_type` in `config.yaml` is incorrectly set. Ensure that it matches your phone's hook switch type (NC or NO).
### Verify default audio interface ### Verify default audio interface
A few users had issues where audio I/O was defaulting to HDMI. To alleviate this, check the following: A few users had issues where audio I/O was defaulting to HDMI. To alleviate this, check the following:
#### Check the Sound Card Configuration: #### Check the Sound Card Configuration
Verify the available sound devices using the following command: Verify the available sound devices using the following command:
@ -212,7 +230,7 @@ aplay -l
_Ensure that your USB audio interface is listed and note the card and device numbers._ _Ensure that your USB audio interface is listed and note the card and device numbers._
#### Set the Default Sound Card: #### Set the Default Sound Card
If you want to route audio through your USB audio interface, you'll need to make it the default sound card. If you want to route audio through your USB audio interface, you'll need to make it the default sound card.
Edit the ALSA configuration file (usually located at `/etc/asound.conf` or `~/.asoundrc`) and add the following: Edit the ALSA configuration file (usually located at `/etc/asound.conf` or `~/.asoundrc`) and add the following:
@ -232,5 +250,5 @@ sudo /etc/init.d/alsa-utils restart
## Support ## Support
If this code helped you or if you have some feedback, I'd be thrilled to [hear about it](mailto:dillpicholas@duck.com)! If this code helped you or if you have some feedback, I'd be thrilled to [hear about it](mailto:dillpicholas@duck.com)!
Feel like saying thanks? You can [buy me a coffee](https://www.buymeacoffee.com/dillpicholas) ☕. Feel like saying thanks? You can [buy me a coffee](https://www.buymeacoffee.com/dillpicholas) ☕.

View File

@ -1,71 +1,97 @@
#! /usr/bin/env python3 #! /usr/bin/env python3
import audioInterface import logging
import os
import yaml
import sys import sys
from datetime import datetime from datetime import datetime
from gpiozero import Button from pathlib import Path
from signal import pause from signal import pause
from pydub import AudioSegment
from pydub.playback import play
try: import pyaudio
with open("config.yaml") as f: import yaml
config = yaml.load(f, Loader=yaml.FullLoader) from gpiozero import Button
except FileNotFoundError as e: from pydub import AudioSegment, playback
print(
f"Could not find the config.yaml file. FileNotFoundError: {e}. Check config location and retry."
)
sys.exit(1)
hook = Button(config["hook_gpio"]) import audioInterface
# Set up logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
BASE_DIR = Path(__file__).parent
CONFIG_PATH = BASE_DIR / "config.yaml"
FORMATS = {
"INT16": pyaudio.paInt16,
"INT32": pyaudio.paInt32,
"FLOAT32": pyaudio.paFloat32,
}
def off_hook() -> None: def load_config():
print("Phone off hook, ready to begin!") try:
audio_interface = audioInterface.AudioInterface(config, hook) with CONFIG_PATH.open() as f:
return yaml.safe_load(f)
# playback voice message through speaker except FileNotFoundError as e:
print("Playing voicemail message...") logger.error(
play( f"Could not find {CONFIG_PATH}. FileNotFoundError: {e}. Check config location and retry."
AudioSegment.from_wav(
os.path.dirname(os.path.abspath(config["source_file"]))
+ "/sounds/voicemail.wav"
) )
- config["playback_reduction"] sys.exit(1)
def play_audio(filename, reduction=0):
try:
sound_path = BASE_DIR / "sounds" / filename
sound = AudioSegment.from_wav(sound_path) - reduction
playback.play(sound)
except Exception as e:
logger.error(f"Error playing {filename}. Error: {e}")
def off_hook():
global hook, config
logger.info("Phone off hook, ready to begin!")
audio_interface = audioInterface.AudioInterface(
hook=hook,
buffer_size=config["buffer_size"],
channels=config["channels"],
format=FORMATS.get(config["format"], pyaudio.paInt16),
sample_rate=config["sample_rate"],
recording_limit=config["recording_limit"],
dev_index=config["alsa_hw_mapping"],
hook_type=config["hook_type"],
) )
# start recording beep logger.info("Playing voicemail message...")
print("Playing beep...") play_audio("voicemail.wav", config["playback_reduction"])
play(
AudioSegment.from_wav(
os.path.dirname(os.path.abspath(config["source_file"])) + "/sounds/beep.wav"
)
- config["beep_reduction"]
)
# now, while phone is off the hook, record audio from the microphone logger.info("Playing beep...")
print("recording") play_audio("beep.wav", config["beep_reduction"])
logger.info("recording")
audio_interface.record() audio_interface.record()
audio_interface.stop() audio_interface.stop()
output_file = (
os.path.dirname(os.path.abspath(config["source_file"])) output_file = str(BASE_DIR / "recordings" / f"{datetime.now().isoformat()}.wav")
+ "/recordings/" audio_interface.close(output_file)
+ f"{datetime.now().isoformat()}" logger.info("Finished recording!")
)
audio_interface.close(output_file + ".wav")
print("Finished recording!")
def on_hook() -> None: def on_hook():
print("Phone on hook.\nSleeping...") logger.info("Phone on hook.\nSleeping...")
def main(): def main():
hook.when_pressed = off_hook global config, hook
hook.when_released = on_hook config = load_config()
if config["hook_type"] == "NC":
hook = Button(config["hook_gpio"], pull_up=True)
hook.when_pressed = on_hook
hook.when_released = off_hook
else: # Assuming NO if not NC
hook = Button(config["hook_gpio"], pull_up=False)
hook.when_pressed = off_hook
hook.when_released = on_hook
pause() pause()

View File

@ -1,11 +0,0 @@
[Unit]
Description=Rotary Phone Guest Book Project
After=multi-user.target
[Service]
WorkingDirectory=/home/<username>/Desktop/rotaryGuestBook
User=<username>
Type=simple
Restart=always
ExecStart=/usr/bin/env python3 /home/<username>/Desktop/rotaryGuestBook/rotaryGuestBook.py
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,12 @@
[Unit]
Description=Rotary Phone Guest Book Project
After=multi-user.target
[Service]
WorkingDirectory=<path-to-project>
Type=simple
Restart=always
ExecStart=/usr/bin/env python3 <path-to-project>/audioGuestBook.py
[Install]
WantedBy=multi-user.target

View File

@ -1,28 +1,54 @@
#! /usr/bin/env python3 #! /usr/bin/env python3
import pyaudio import logging
import time import time
import wave import wave
from typing import List
import pyaudio
from pydub import AudioSegment from pydub import AudioSegment
from pydub.effects import normalize, compress_dynamic_range from pydub.effects import compress_dynamic_range, normalize
from pydub.scipy_effects import band_pass_filter from pydub.scipy_effects import band_pass_filter
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class AudioInterface: class AudioInterface:
def __init__(self, config, hook) -> None: def __init__(
self.audio = pyaudio.PyAudio() self,
self.chunk = config["buffer_size"] hook,
self.chans = config["channels"] buffer_size,
self.format = pyaudio.paInt16 # 16-bit resolution channels,
self.frames = [] # raw data frames recorded from mic format,
sample_rate,
recording_limit,
dev_index,
hook_type,
) -> None:
self.chunk = buffer_size
self.chans = channels
self.format = format
self.frames: List[bytes] = []
self.hook = hook self.hook = hook
self.samp_rate = config["sample_rate"] self.samp_rate = sample_rate
self.recording_limit = config["recording_limit"] self.recording_limit = recording_limit
self.dev_index = config["alsa_hw_mapping"] # device index found by p.get_device_info_by_index(ii) self.dev_index = dev_index
self.hook_type = hook_type
self.audio = None
self.stream = None
def init_audio(self):
if self.audio is None:
self.audio = pyaudio.PyAudio()
if self.stream is not None:
self.stream.stop_stream()
self.stream.close()
self.stream = None
def record(self): def record(self):
# create pyaudio stream self.init_audio()
self.stream = self.audio.open( self.stream = self.audio.open(
format=self.format, format=self.format,
rate=self.samp_rate, rate=self.samp_rate,
@ -31,68 +57,76 @@ class AudioInterface:
input=True, input=True,
frames_per_buffer=self.chunk, frames_per_buffer=self.chunk,
) )
# loop through stream and append audio chunks to frame array # loop through stream and append audio chunks to frame array
try: try:
start = time.time() start = time.time()
while self.hook.is_pressed: while self.off_hook_condition():
if time.time() - start < self.recording_limit: if time.time() - start < self.recording_limit:
data = self.stream.read(self.chunk, exception_on_overflow=True) data = self.stream.read(self.chunk, exception_on_overflow=True)
self.frames.append(data) self.frames.append(data)
else: else:
break break
except KeyboardInterrupt: except KeyboardInterrupt:
print("Done recording") logger.info("Done recording")
except Exception as e: except Exception as e:
print(str(e)) logger.error(str(e))
def off_hook_condition(self):
if self.hook_type == "NC":
return not self.hook.is_pressed
else: # Assuming default is "NO" if not "NC"
return self.hook.is_pressed
def play(self, file): def play(self, file):
self.wf = wave.open(file, "rb") self.init_audio()
self.stream = self.audio.open( with wave.open(file, "rb") as wf:
format=self.audio.get_format_from_width(self.wf.getsampwidth()), self.stream = self.audio.open(
channels=self.wf.getnchannels(), format=self.audio.get_format_from_width(wf.getsampwidth()),
rate=self.wf.getframerate(), channels=wf.getnchannels(),
output=True, rate=wf.getframerate(),
) output=True,
""" Play entire file """ )
data = self.wf.readframes(self.chunk) data = wf.readframes(self.chunk)
while len(data): while data:
self.stream.write(data) self.stream.write(data)
data = self.wf.readframes(self.chunk) data = wf.readframes(self.chunk)
def stop(self): def stop(self):
# stop the stream if self.stream:
self.stream.stop_stream() self.stream.stop_stream()
# close it self.stream.close()
self.stream.close()
# terminate the pyaudio instantiation if self.audio:
self.audio.terminate() self.audio.terminate()
def close(self, output_file): def close(self, output_file):
# save the audio frames as .wav file try:
with wave.open(output_file, "wb") as wavefile: with wave.open(output_file, "wb") as wavefile:
wavefile.setnchannels(self.chans) wavefile.setnchannels(self.chans)
wavefile.setsampwidth(self.audio.get_sample_size(self.format)) wavefile.setsampwidth(self.audio.get_sample_size(self.format))
wavefile.setframerate(self.samp_rate) wavefile.setframerate(self.samp_rate)
wavefile.writeframes(b"".join(self.frames)) wavefile.writeframes(b"".join(self.frames))
except OSError as e:
logger.error(f"Error writing to file {output_file}. Error: {e}")
def postProcess(self, outputFile): def postProcess(self, outputFile):
"""
TODO: Evaluate whether this is worthwhile...
"""
source = AudioSegment.from_wav(outputFile + ".wav") source = AudioSegment.from_wav(outputFile + ".wav")
filtered = self.filter_audio(source)
normalized = self.normalize_audio(filtered)
compressed = self.compress_audio(normalized)
print("Filtering...")
filtered = band_pass_filter(source, 300, 10000)
print("Normalizing...")
normalized = normalize(filtered)
print("Compress Dynamic Range")
compressed = compress_dynamic_range(normalized)
print("Exporting normalized")
normalized.export(outputFile + "normalized.wav", format="wav") normalized.export(outputFile + "normalized.wav", format="wav")
print("Exporting compressed")
compressed.export(outputFile + "compressed.mp3", format="mp3") compressed.export(outputFile + "compressed.mp3", format="mp3")
print("Finished...") def filter_audio(self, audio):
logger.info("Filtering...")
return band_pass_filter(audio, 300, 10000)
def normalize_audio(self, audio):
logger.info("Normalizing...")
return normalize(audio)
def compress_audio(self, audio):
logger.info("Compress Dynamic Range")
return compress_dynamic_range(audio)

View File

@ -9,4 +9,6 @@ rotary_gpio: 23
rotary_hold_repeat: true rotary_hold_repeat: true
rotary_hold_time: 0.25 rotary_hold_time: 0.25
sample_rate: 44100 sample_rate: 44100
source_file: "audioGuestBook.py" source_file: audioGuestBook.py
format: INT16
hook_type: NC # or 'NO'

68
installer.sh Normal file
View File

@ -0,0 +1,68 @@
#!/bin/bash
# Dependency installation
echo "Installing dependencies..."
sudo apt-get update
if ! sudo apt-get install -y python3-pip python3-gpiozero; then
echo "Failed to install system packages."
exit 1
fi
# Use --user flag for pip installations
if ! pip3 install --user pydub pyaudio PyYAML; then
echo "Failed to install Python packages."
exit 1
fi
# Get the directory of the currently executing script
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Ensure the directory exists
if [[ ! -d "$DIR" ]]; then
echo "Error: Directory $DIR does not exist."
exit 1
fi
# Change ownership of the entire project directory to the current user
if ! sudo chown -R $USER:$USER "$DIR"; then
echo "Failed to change ownership of $DIR."
exit 1
fi
# Replace placeholders in the service file and save to temporary location
if ! sed "s|<path-to-project>|$DIR|g" "$DIR/audioGuestBook.service.template" >/tmp/audioGuestBook.service; then
echo "sed command failed."
exit 1
fi
# Move the modified service file to systemd directory
if ! sudo mv /tmp/audioGuestBook.service /etc/systemd/system/; then
echo "Failed to move service file."
exit 1
fi
# Create required directories
if ! sudo mkdir -p "$DIR/recordings"; then
echo "Failed to create directories."
exit 1
fi
# Set execution permissions for the main script
if ! sudo chmod +x "$DIR/audioGuestBook.py"; then
echo "Failed to set script permissions."
exit 1
fi
# Reload systemd, unmask, enable and start the service
sudo systemctl daemon-reload
sudo systemctl unmask audioGuestBook.service
if ! sudo systemctl enable audioGuestBook.service; then
echo "Failed to enable the service."
exit 1
fi
if ! sudo systemctl start audioGuestBook.service; then
echo "Failed to start the service."
exit 1
fi
echo "Installation completed successfully!"

View File

@ -1,4 +0,0 @@
gpiozero==1.6.2
pyaudio==0.2.12
pydub==0.25.1
PyYAML==6.0