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
3
.gitignore
vendored
3
.gitignore
vendored
@ -1 +1,2 @@
|
||||
*.code*
|
||||
*.code*
|
||||
*.trunk*
|
142
README.md
142
README.md
@ -12,13 +12,18 @@
|
||||
- [Microphone Replacement (Optional)](#microphone-replacement-optional)
|
||||
- [Software](#software)
|
||||
- [Dev Environment](#dev-environment)
|
||||
- [Dependencies](#dependencies)
|
||||
- [Installation](#installation)
|
||||
- [audioGuestBook systemctl service](#audioguestbook-systemctl-service)
|
||||
- [Config](#config)
|
||||
- [AudioInterface Class](#audiointerface-class)
|
||||
- [audioGuestBook systemctl service](#audioguestbook-systemctl-service)
|
||||
- [Operation Mode 1: audioGuestBook](#operation-mode-1-audioguestbook)
|
||||
- [Operation Mode 2: audioGuestBookwithRotaryDialer](#operation-mode-2-audioguestbookwithrotarydialer)
|
||||
- [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)
|
||||
|
||||
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)
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
| 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 |
|
||||
| [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 |
|
||||
| [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 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 ---** | --- | --- |
|
||||
| [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 ---** | --- | --- |
|
||||
| [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 Charger](https://www.adafruit.com/product/1904) | Optional: for re-charging the LiPo. |1| $6.95 |
|
||||
| --- | **--- 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 |
|
||||
| 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 |
|
||||
| [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 |
|
||||
| [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 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 ---** | --- | --- |
|
||||
| [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 ---** | --- | --- |
|
||||
| [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 Charger](https://www.adafruit.com/product/1904) | Optional: for re-charging the LiPo. | 1 | $6.95 |
|
||||
| --- | **--- 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 |
|
||||
|
||||
## Hardware
|
||||
|
||||
@ -67,28 +72,35 @@ I would also like to thread the audio playback so I can have a monitor/watchdog
|
||||
|
||||
#### 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:
|
||||
|
||||
| On-hook --> Open circuit (Value == 1) | Off-hook --> Current flowing |
|
||||
| ------------- | ------------- |
|
||||
| ![image](images/hook_test_1.jpg) | ![image](images/hook_test_2.jpg) |
|
||||
|
||||
| On-hook --> Open circuit (Value == 1) | Off-hook --> Current flowing |
|
||||
| ------------------------------------- | -------------------------------- |
|
||||
| ![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 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)
|
||||
|
||||
- *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
|
||||
|
||||
- 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.
|
||||
- *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)
|
||||
|
||||
@ -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.
|
||||
|
||||
### Dependencies
|
||||
### Installation
|
||||
|
||||
- `pip3 install -r requirements.txt` or pip install each manually:
|
||||
- [GPIOZero](https://gpiozero.readthedocs.io)
|
||||
- [Pydub](http://pydub.com/)
|
||||
- [PyAudio](https://people.csail.mit.edu/hubert/pyaudio/)
|
||||
- [PyYAML](https://pyyaml.org/)
|
||||
After cloning the repository, there's an installer script available to ease the setup process. This script takes care of several tasks:
|
||||
|
||||
1. Install required dependencies.
|
||||
2. Replace placeholders in the service file to adapt to your project directory.
|
||||
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)
|
||||
|
||||
@ -146,39 +181,18 @@ To replace:
|
||||
- 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:
|
||||
![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)
|
||||
|
||||
- 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.
|
||||
|
||||
#### [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)
|
||||
|
||||
- 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).
|
||||
- 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.
|
||||
- On hook (depressed)
|
||||
- Nothing happens
|
||||
@ -189,7 +203,7 @@ systemctl start audioGuestBook.service
|
||||
|
||||
### 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.
|
||||
- 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
|
||||
|
||||
### 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
|
||||
|
||||
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:
|
||||
|
||||
@ -212,7 +230,7 @@ aplay -l
|
||||
|
||||
_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.
|
||||
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
|
||||
|
||||
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) ☕.
|
||||
|
@ -1,71 +1,97 @@
|
||||
#! /usr/bin/env python3
|
||||
|
||||
import audioInterface
|
||||
import os
|
||||
import yaml
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from datetime import datetime
|
||||
from gpiozero import Button
|
||||
from pathlib import Path
|
||||
from signal import pause
|
||||
from pydub import AudioSegment
|
||||
from pydub.playback import play
|
||||
|
||||
try:
|
||||
with open("config.yaml") as f:
|
||||
config = yaml.load(f, Loader=yaml.FullLoader)
|
||||
except FileNotFoundError as e:
|
||||
print(
|
||||
f"Could not find the config.yaml file. FileNotFoundError: {e}. Check config location and retry."
|
||||
)
|
||||
sys.exit(1)
|
||||
import pyaudio
|
||||
import yaml
|
||||
from gpiozero import Button
|
||||
from pydub import AudioSegment, playback
|
||||
|
||||
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:
|
||||
print("Phone off hook, ready to begin!")
|
||||
audio_interface = audioInterface.AudioInterface(config, hook)
|
||||
|
||||
# playback voice message through speaker
|
||||
print("Playing voicemail message...")
|
||||
play(
|
||||
AudioSegment.from_wav(
|
||||
os.path.dirname(os.path.abspath(config["source_file"]))
|
||||
+ "/sounds/voicemail.wav"
|
||||
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."
|
||||
)
|
||||
- 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
|
||||
print("Playing beep...")
|
||||
play(
|
||||
AudioSegment.from_wav(
|
||||
os.path.dirname(os.path.abspath(config["source_file"])) + "/sounds/beep.wav"
|
||||
)
|
||||
- config["beep_reduction"]
|
||||
)
|
||||
logger.info("Playing voicemail message...")
|
||||
play_audio("voicemail.wav", config["playback_reduction"])
|
||||
|
||||
# now, while phone is off the hook, record audio from the microphone
|
||||
print("recording")
|
||||
logger.info("Playing beep...")
|
||||
play_audio("beep.wav", config["beep_reduction"])
|
||||
|
||||
logger.info("recording")
|
||||
audio_interface.record()
|
||||
audio_interface.stop()
|
||||
output_file = (
|
||||
os.path.dirname(os.path.abspath(config["source_file"]))
|
||||
+ "/recordings/"
|
||||
+ f"{datetime.now().isoformat()}"
|
||||
)
|
||||
audio_interface.close(output_file + ".wav")
|
||||
print("Finished recording!")
|
||||
|
||||
output_file = str(BASE_DIR / "recordings" / f"{datetime.now().isoformat()}.wav")
|
||||
audio_interface.close(output_file)
|
||||
logger.info("Finished recording!")
|
||||
|
||||
|
||||
def on_hook() -> None:
|
||||
print("Phone on hook.\nSleeping...")
|
||||
def on_hook():
|
||||
logger.info("Phone on hook.\nSleeping...")
|
||||
|
||||
|
||||
def main():
|
||||
hook.when_pressed = off_hook
|
||||
hook.when_released = on_hook
|
||||
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_released = on_hook
|
||||
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
|
||||
|
||||
import pyaudio
|
||||
import logging
|
||||
import time
|
||||
import wave
|
||||
from typing import List
|
||||
|
||||
import pyaudio
|
||||
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
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AudioInterface:
|
||||
def __init__(self, config, hook) -> None:
|
||||
self.audio = pyaudio.PyAudio()
|
||||
self.chunk = config["buffer_size"]
|
||||
self.chans = config["channels"]
|
||||
self.format = pyaudio.paInt16 # 16-bit resolution
|
||||
self.frames = [] # raw data frames recorded from mic
|
||||
def __init__(
|
||||
self,
|
||||
hook,
|
||||
buffer_size,
|
||||
channels,
|
||||
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.samp_rate = config["sample_rate"]
|
||||
self.recording_limit = config["recording_limit"]
|
||||
self.dev_index = config["alsa_hw_mapping"] # device index found by p.get_device_info_by_index(ii)
|
||||
self.samp_rate = sample_rate
|
||||
self.recording_limit = recording_limit
|
||||
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):
|
||||
# create pyaudio stream
|
||||
self.init_audio()
|
||||
self.stream = self.audio.open(
|
||||
format=self.format,
|
||||
rate=self.samp_rate,
|
||||
@ -31,68 +57,76 @@ class AudioInterface:
|
||||
input=True,
|
||||
frames_per_buffer=self.chunk,
|
||||
)
|
||||
|
||||
# loop through stream and append audio chunks to frame array
|
||||
try:
|
||||
start = time.time()
|
||||
while self.hook.is_pressed:
|
||||
while self.off_hook_condition():
|
||||
if time.time() - start < self.recording_limit:
|
||||
data = self.stream.read(self.chunk, exception_on_overflow=True)
|
||||
self.frames.append(data)
|
||||
else:
|
||||
break
|
||||
except KeyboardInterrupt:
|
||||
print("Done recording")
|
||||
logger.info("Done recording")
|
||||
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):
|
||||
self.wf = wave.open(file, "rb")
|
||||
self.stream = self.audio.open(
|
||||
format=self.audio.get_format_from_width(self.wf.getsampwidth()),
|
||||
channels=self.wf.getnchannels(),
|
||||
rate=self.wf.getframerate(),
|
||||
output=True,
|
||||
)
|
||||
""" Play entire file """
|
||||
data = self.wf.readframes(self.chunk)
|
||||
while len(data):
|
||||
self.stream.write(data)
|
||||
data = self.wf.readframes(self.chunk)
|
||||
self.init_audio()
|
||||
with wave.open(file, "rb") as wf:
|
||||
self.stream = self.audio.open(
|
||||
format=self.audio.get_format_from_width(wf.getsampwidth()),
|
||||
channels=wf.getnchannels(),
|
||||
rate=wf.getframerate(),
|
||||
output=True,
|
||||
)
|
||||
data = wf.readframes(self.chunk)
|
||||
while data:
|
||||
self.stream.write(data)
|
||||
data = wf.readframes(self.chunk)
|
||||
|
||||
def stop(self):
|
||||
# stop the stream
|
||||
self.stream.stop_stream()
|
||||
# close it
|
||||
self.stream.close()
|
||||
# terminate the pyaudio instantiation
|
||||
self.audio.terminate()
|
||||
if self.stream:
|
||||
self.stream.stop_stream()
|
||||
self.stream.close()
|
||||
|
||||
if self.audio:
|
||||
self.audio.terminate()
|
||||
|
||||
def close(self, output_file):
|
||||
# save the audio frames as .wav file
|
||||
with wave.open(output_file, "wb") as wavefile:
|
||||
wavefile.setnchannels(self.chans)
|
||||
wavefile.setsampwidth(self.audio.get_sample_size(self.format))
|
||||
wavefile.setframerate(self.samp_rate)
|
||||
wavefile.writeframes(b"".join(self.frames))
|
||||
try:
|
||||
with wave.open(output_file, "wb") as wavefile:
|
||||
wavefile.setnchannels(self.chans)
|
||||
wavefile.setsampwidth(self.audio.get_sample_size(self.format))
|
||||
wavefile.setframerate(self.samp_rate)
|
||||
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):
|
||||
"""
|
||||
TODO: Evaluate whether this is worthwhile...
|
||||
"""
|
||||
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")
|
||||
|
||||
print("Exporting compressed")
|
||||
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_time: 0.25
|
||||
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