Develop (#12)
* 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:
parent
3b5e32f781
commit
3eddb1c300
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +1,2 @@
|
|||||||
*.code*
|
*.code*
|
||||||
|
*.trunk*
|
106
README.md
106
README.md
@ -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,15 +42,15 @@ 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 |
|
||||||
@ -55,9 +60,9 @@ I would also like to thread the audio playback so I can have a monitor/watchdog
|
|||||||
| --- | **--- 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&=&crid=15WZEWMZ17EM9&=&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&=&crid=15WZEWMZ17EM9&=&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 |
|
||||||
|
|
||||||
@ -67,10 +72,17 @@ 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.
|
||||||
@ -78,17 +90,17 @@ I would also like to thread the audio playback so I can have a monitor/watchdog
|
|||||||
|
|
||||||
![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 |
|
| Rotary Phone Block Terminal | Top-down view |
|
||||||
| ------------- | ------------- |
|
| ----------------------------------- | -------------------------------------------- |
|
||||||
| ![image](images/block_terminal.jpg) | ![image](images/top_view_block_terminal.jpg) |
|
| ![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:
|
||||||
|
@ -1,69 +1,95 @@
|
|||||||
#! /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."
|
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 load_config():
|
||||||
|
try:
|
||||||
|
with CONFIG_PATH.open() as f:
|
||||||
|
return yaml.safe_load(f)
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
logger.error(
|
||||||
|
f"Could not find {CONFIG_PATH}. FileNotFoundError: {e}. Check config location and retry."
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
hook = Button(config["hook_gpio"])
|
|
||||||
|
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() -> None:
|
def off_hook():
|
||||||
print("Phone off hook, ready to begin!")
|
global hook, config
|
||||||
audio_interface = audioInterface.AudioInterface(config, hook)
|
|
||||||
|
|
||||||
# playback voice message through speaker
|
logger.info("Phone off hook, ready to begin!")
|
||||||
print("Playing voicemail message...")
|
|
||||||
play(
|
audio_interface = audioInterface.AudioInterface(
|
||||||
AudioSegment.from_wav(
|
hook=hook,
|
||||||
os.path.dirname(os.path.abspath(config["source_file"]))
|
buffer_size=config["buffer_size"],
|
||||||
+ "/sounds/voicemail.wav"
|
channels=config["channels"],
|
||||||
)
|
format=FORMATS.get(config["format"], pyaudio.paInt16),
|
||||||
- config["playback_reduction"]
|
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():
|
||||||
|
global config, 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_pressed = off_hook
|
||||||
hook.when_released = on_hook
|
hook.when_released = on_hook
|
||||||
pause()
|
pause()
|
||||||
|
@ -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
|
|
12
audioGuestBook.service.template
Normal file
12
audioGuestBook.service.template
Normal 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
|
@ -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()
|
||||||
|
with wave.open(file, "rb") as wf:
|
||||||
self.stream = self.audio.open(
|
self.stream = self.audio.open(
|
||||||
format=self.audio.get_format_from_width(self.wf.getsampwidth()),
|
format=self.audio.get_format_from_width(wf.getsampwidth()),
|
||||||
channels=self.wf.getnchannels(),
|
channels=wf.getnchannels(),
|
||||||
rate=self.wf.getframerate(),
|
rate=wf.getframerate(),
|
||||||
output=True,
|
output=True,
|
||||||
)
|
)
|
||||||
""" Play entire file """
|
data = wf.readframes(self.chunk)
|
||||||
data = self.wf.readframes(self.chunk)
|
while data:
|
||||||
while len(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)
|
||||||
|
@ -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
68
installer.sh
Normal 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!"
|
@ -1,4 +0,0 @@
|
|||||||
gpiozero==1.6.2
|
|
||||||
pyaudio==0.2.12
|
|
||||||
pydub==0.25.1
|
|
||||||
PyYAML==6.0
|
|
Loading…
Reference in New Issue
Block a user