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:
		
							
								
								
									
										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 | | ||||
| | ------------- | ------------- | | ||||
| |  |   | | ||||
|  | ||||
| | On-hook --> Open circuit (Value == 1) | Off-hook --> Current flowing     | | ||||
| | ------------------------------------- | -------------------------------- | | ||||
| |       |  | | ||||
|  | ||||
| - 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. | ||||
|  | ||||
|    | ||||
|  | ||||
| - *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 | | ||||
| | ------------- | ------------- | | ||||
| |  |   | | ||||
|    | ||||
| #### 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_ | ||||
|  | ||||
|  | ||||
|  | ||||
| @@ -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: | ||||
|    | ||||
| - **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 | ||||
		Reference in New Issue
	
	Block a user
	 Nick Pourazima
					Nick Pourazima