Merge develop branch into main (#1)
* refactor: functional * refactor: ♻️ move alsa_hw_mapping to config param --------- Co-authored-by: Nick Pourazima <nick.pourazima@gmail.com>
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,2 +1 @@ | ||||
| *.wav | ||||
| *.mp3 | ||||
| *.code* | ||||
							
								
								
									
										112
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,112 @@ | ||||
| # Rotary Phone Audio Guestbook | ||||
|  | ||||
| This project transforms a rotary phone into a voice recorder for use at special events (i.e. wedding audio guestbook, etc.). | ||||
|  | ||||
| - [Rotary Phone Audio Guestbook](#rotary-phone-audio-guestbook) | ||||
|   - [Background](#background) | ||||
|   - [Post-Event](#post-event) | ||||
|     - [Future Potential](#future-potential) | ||||
|   - [Materials](#materials) | ||||
|   - [Setup](#setup) | ||||
|     - [Hardware](#hardware) | ||||
|       - [Wiring](#wiring) | ||||
|       - [Microphone Replacement (Optional)](#microphone-replacement-optional) | ||||
|     - [Software](#software) | ||||
|       - [Dev Environment](#dev-environment) | ||||
|       - [Dependencies](#dependencies) | ||||
|       - [AudioInterface Class](#audiointerface-class) | ||||
|       - [rotaryGuestBook.service](#rotaryguestbookservice) | ||||
|       - [Operation Mode 1: rotaryGuestBook](#operation-mode-1-rotaryguestbook) | ||||
|       - [Operation Mode 2: rotaryGuestBookwithRotaryDialer](#operation-mode-2-rotaryguestbookwithrotarydialer) | ||||
|  | ||||
| ## Background | ||||
|  | ||||
| I was inspired by my own upcoming wedding to put together a DIY solution for an audio guestbook using a rotary phone. Most online rentals were charging $600 for an experience that didn't even offer the ability to add a custom voice mail and took about 4-6 weeks of turn around time to process the audio after the event. I tried to use as many parts that I had laying around to keep costs down. It worked out quite well and we were able to gather some very special voice messages. | ||||
|  | ||||
| Below you will find a parts list and detailed setup guide. Please feel free to reach out to me with any questions. | ||||
|  | ||||
| ## Post-Event | ||||
|  | ||||
| Since this was a trial by fire type of scenario there ended up being a few gotchas at the real event which I've since accounted for. Namely setting a time limit on the recording length as we had some youngsters leaving 5+ minute messages repeatedly and this ended up draining the battery. | ||||
|  | ||||
| ### Future Potential | ||||
|  | ||||
| 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 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. | ||||
|  | ||||
| ## 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 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 | | ||||
|  | ||||
| ## Setup | ||||
|  | ||||
| ### Hardware | ||||
|  | ||||
| #### Wiring | ||||
|  | ||||
| #### Microphone Replacement (Optional) | ||||
|  | ||||
| I found the sound quality of the built-in [carbon microphone](https://en.wikipedia.org/wiki/Carbon_microphone) on the rotary phone to be quite lacking in terms of amplitude, dynamic range and overall vocal quality. I tried boosting the gain from the digital (ALSA driver) side but this introduced an incredible amount of noise as expected. I then approached this from the analog domain and tried alternative circuitry to boost the sound quality based off this [carbon-to-dynamic converter](https://www.circuits-diy.com/mic-converter-circuit/). | ||||
|  | ||||
| Might be worth a further investigation in the future since it retains the integrity of the original rotary phone. | ||||
|  | ||||
| My final attempt involved the introduction of some post-proceesing (see dev branch) to bandpass some of the freqs outside the speech domain and add some normalization. The processing was costly in terms of processing and power consumption/rendering time and I ultimately decided it was worth acquiring something that yielded a better capture right out the gate. Crap in, crap out - as they say in the sound recording industry. | ||||
|  | ||||
| To replace: | ||||
|  | ||||
| - Unscrew mouthpiece and remove the carbon mic | ||||
| - Pop out the plastic terminal housing with the two metal leads | ||||
| - Unscrew red and black wires from terminal | ||||
|  | ||||
|  | ||||
|  | ||||
| ### Software | ||||
|  | ||||
| #### Dev Environment | ||||
|  | ||||
| - rpi image: [Rasbian](https://www.raspberrypi.com/documentation/computers/getting-started.html) w/ SSH enabled | ||||
| - rpi on same network as development machine | ||||
| - Desktop IDE: vscode w/ [SSH FS extension](https://marketplace.visualstudio.com/items?itemName=Kelvin.vscode-sshfs) | ||||
|  | ||||
| [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 | ||||
|  | ||||
| - `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/) | ||||
|  | ||||
| #### [AudioInterface Class](audioInterface.py) | ||||
|  | ||||
| #### [rotaryGuestBook.service](rotaryGuestBook.service) | ||||
|  | ||||
| This service starts the python script on boot. Place it in the `/etc/systemd/system` directory. | ||||
|  | ||||
| `systemctl enable audioGuestBook.service` | ||||
| `systemctl start audioGuestBook.service` | ||||
|  | ||||
|  | ||||
| #### Operation Mode 1: [rotaryGuestBook](rotaryGuestBook.py) | ||||
|  | ||||
| #### Operation Mode 2: [rotaryGuestBookwithRotaryDialer](rotaryGuestBookwithRotaryDialer.py) | ||||
|  | ||||
| ***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). | ||||
| - In this mode of operation the users will need to dial 0 on the rotary dialer in order to initiate the voicemail. | ||||
							
								
								
									
										73
									
								
								audioGuestBook.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,73 @@ | ||||
| #! /usr/bin/env python3 | ||||
|  | ||||
| import audioInterface | ||||
| import os | ||||
| import yaml | ||||
| import sys | ||||
|  | ||||
| from datetime import datetime | ||||
| from gpiozero import Button | ||||
| 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) | ||||
|  | ||||
| hook = Button(config["hook_gpio"]) | ||||
|  | ||||
|  | ||||
| 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" | ||||
|         ) | ||||
|         - config["playback_reduction"] | ||||
|     ) | ||||
|  | ||||
|     # 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"] | ||||
|     ) | ||||
|  | ||||
|     # now, while phone is off the hook, record audio from the microphone | ||||
|     print("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!") | ||||
|  | ||||
|  | ||||
| def on_hook() -> None: | ||||
|     print("Phone on hook.\nSleeping...") | ||||
|  | ||||
|  | ||||
| def main(): | ||||
|     hook.when_pressed = off_hook | ||||
|     hook.when_released = on_hook | ||||
|     pause() | ||||
|  | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     main() | ||||
							
								
								
									
										11
									
								
								audioGuestBook.service
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,11 @@ | ||||
| [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 | ||||
							
								
								
									
										98
									
								
								audioInterface.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,98 @@ | ||||
| #! /usr/bin/env python3 | ||||
|  | ||||
| import pyaudio | ||||
| import time | ||||
| import wave | ||||
| from pydub import AudioSegment | ||||
| from pydub.effects import normalize, compress_dynamic_range | ||||
| from pydub.scipy_effects import band_pass_filter | ||||
|  | ||||
|  | ||||
|  | ||||
| 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 | ||||
|         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) | ||||
|  | ||||
|     def record(self): | ||||
|         # create pyaudio stream | ||||
|         self.stream = self.audio.open( | ||||
|             format=self.format, | ||||
|             rate=self.samp_rate, | ||||
|             channels=self.chans, | ||||
|             input_device_index=self.dev_index, | ||||
|             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: | ||||
|                 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") | ||||
|         except Exception as e: | ||||
|             print(str(e)) | ||||
|  | ||||
|     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) | ||||
|  | ||||
|     def stop(self): | ||||
|         # stop the stream | ||||
|         self.stream.stop_stream() | ||||
|         # close it | ||||
|         self.stream.close() | ||||
|         # terminate the pyaudio instantiation | ||||
|         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)) | ||||
|  | ||||
|     def postProcess(self, outputFile): | ||||
|         """ | ||||
|         TODO: Evaluate whether this is worthwhile... | ||||
|         """ | ||||
|         source = AudioSegment.from_wav(outputFile + ".wav") | ||||
|  | ||||
|         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...") | ||||
							
								
								
									
										17
									
								
								config.yaml
									
									
									
									
									
								
							
							
						
						| @@ -1,9 +1,12 @@ | ||||
| hook_gpio: 22 | ||||
| rotary_gpio: 3 | ||||
| rotary_hold_time: 0.25 | ||||
| rotary_hold_repeat: true | ||||
| playback_reduction: 16 | ||||
| alsa_hw_mapping: 1 | ||||
| beep_reduction: 24 | ||||
| sample_rate: 44100 | ||||
| buffer_size: 4096 | ||||
| channels: 2 | ||||
| channels: 2 | ||||
| hook_gpio: 22 | ||||
| playback_reduction: 16 | ||||
| recording_limit: 60 | ||||
| rotary_gpio: 23 | ||||
| rotary_hold_repeat: true | ||||
| rotary_hold_time: 0.25 | ||||
| sample_rate: 44100 | ||||
| source_file: "audioGuestBook.py" | ||||
							
								
								
									
										
											BIN
										
									
								
								images/PXL_20221127_191801539.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.9 MiB | 
							
								
								
									
										
											BIN
										
									
								
								images/PXL_20221127_191808218.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.7 MiB | 
							
								
								
									
										
											BIN
										
									
								
								images/PXL_20221127_191822432.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.7 MiB | 
							
								
								
									
										
											BIN
										
									
								
								images/PXL_20221127_191828405.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.0 MiB | 
							
								
								
									
										
											BIN
										
									
								
								images/PXL_20221127_191903412.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.4 MiB | 
							
								
								
									
										
											BIN
										
									
								
								images/PXL_20221217_193545205.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.7 MiB | 
							
								
								
									
										
											BIN
										
									
								
								images/Raspberry-Pi-GPIO-Header-with-Photo-1024x683.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 667 KiB | 
							
								
								
									
										
											BIN
										
									
								
								images/phone2.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 11 KiB | 
							
								
								
									
										4
									
								
								requirements.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,4 @@ | ||||
| gpiozero==1.6.2 | ||||
| pyaudio==0.2.12 | ||||
| pydub==0.25.1 | ||||
| PyYAML==6.0 | ||||
| @@ -1,215 +0,0 @@ | ||||
| #! /usr/bin/env python3 | ||||
|  | ||||
| import os | ||||
| import yaml | ||||
| import pyaudio | ||||
| import wave | ||||
| from datetime import datetime | ||||
| from gpiozero import Button | ||||
| from multiprocessing import Process | ||||
| from signal import pause | ||||
| from pydub.scipy_effects import band_pass_filter | ||||
| from pydub.effects import normalize | ||||
| from pydub import AudioSegment | ||||
| from pydub.playback import play | ||||
|  | ||||
| with open("config.yaml") as f: | ||||
|     config = yaml.load(f, Loader=yaml.FullLoader) | ||||
|  | ||||
| hook = Button(config["hook_gpio"]) | ||||
| # rotaryDial = Button(pin=config['rotary_gpio'], hold_time=config['rotary_hold_time'], hold_repeat=config['rotary_hold_repeat']) | ||||
|  | ||||
| """ | ||||
| TODO: These globals are a temp solution for the rotary dialer, would love to not | ||||
| depend on globals for this logic. | ||||
| """ | ||||
| # count = 0 | ||||
| # dialed = [] | ||||
| # reset_flag = False | ||||
|  | ||||
|  | ||||
| class AudioInterface: | ||||
|     def __init__(self) -> None: | ||||
|         self.audio = pyaudio.PyAudio() | ||||
|         self.samp_rate = config["sample_rate"] | ||||
|         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 record(self): | ||||
|         # create pyaudio stream | ||||
|         dev_index = 1  # device index found by p.get_device_info_by_index(ii) | ||||
|         self.stream = self.audio.open( | ||||
|             format=self.format, | ||||
|             rate=self.samp_rate, | ||||
|             channels=self.chans, | ||||
|             input_device_index=dev_index, | ||||
|             input=True, | ||||
|             frames_per_buffer=self.chunk, | ||||
|         ) | ||||
|         # loop through stream and append audio chunks to frame array | ||||
|         try: | ||||
|             # TODO: either pass hook as object into class, or figure out another cleaner solution | ||||
|             while hook.is_pressed: | ||||
|                 data = self.stream.read(self.chunk, exception_on_overflow=True) | ||||
|                 self.frames.append(data) | ||||
|         except KeyboardInterrupt: | ||||
|             print("Done recording") | ||||
|         except Exception as e: | ||||
|             print(str(e)) | ||||
|  | ||||
|     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) | ||||
|  | ||||
|     def stop(self): | ||||
|         # stop the stream | ||||
|         self.stream.stop_stream() | ||||
|         # close it | ||||
|         self.stream.close() | ||||
|         # terminate the pyaudio instantiation | ||||
|         self.audio.terminate() | ||||
|  | ||||
|     def close(self, outputFile): | ||||
|         # save the audio frames as .wav file | ||||
|         with wave.open(outputFile, "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)) | ||||
|  | ||||
|     def postProcess(self, outputFile): | ||||
|         """ | ||||
|         TODO: Evaluate whether this is worthwhile... | ||||
|         """ | ||||
|         source = AudioSegment.from_wav(outputFile + ".wav") | ||||
|  | ||||
|         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 offHook(): | ||||
|     print("Phone off hook, ready to begin!") | ||||
|     # if dialed and dialed[0] == 0: | ||||
|     audioInterface = AudioInterface() | ||||
|  | ||||
|     # playback voice message through speaker | ||||
|     print("Playing voicemail message...") | ||||
|     play( | ||||
|         AudioSegment.from_wav( | ||||
|             os.path.dirname(os.path.abspath("rotaryGuestBook.py")) | ||||
|             + "/sounds/voicemail.wav" | ||||
|         ) | ||||
|         - config["playback_reduction"] | ||||
|     ) | ||||
|  | ||||
|     # start recording beep | ||||
|     print("Playing beep...") | ||||
|     play( | ||||
|         AudioSegment.from_wav( | ||||
|             os.path.dirname(os.path.abspath("rotaryGuestBook.py")) + "/sounds/beep.wav" | ||||
|         ) | ||||
|         - config["beep_reduction"] | ||||
|     ) | ||||
|  | ||||
|     # now, while phone is not off the hook, record audio from the microphone | ||||
|     print("recording") | ||||
|     audioInterface.record() | ||||
|     audioInterface.stop() | ||||
|     outputFile = ( | ||||
|         os.path.dirname(os.path.abspath("rotaryGuestBook.py")) | ||||
|         + "/recordings/" | ||||
|         + f"{datetime.now().isoformat()}" | ||||
|     ) | ||||
|     audioInterface.close(outputFile + ".wav") | ||||
|     print("finished recording") | ||||
|  | ||||
|     """ | ||||
|     post processing | ||||
|     """ | ||||
|     # print("spawn postProcessing thread") | ||||
|     # Process(target=audioInterface.postProcess, args=(outputFil e,)).start() | ||||
|  | ||||
|     """ | ||||
|     rotary dialer special messages | ||||
|     """ | ||||
|     # if dialed[0:3] == [9,2,7]: | ||||
|     #     # play special vm | ||||
|     #     play(AudioSegment.from_wav(os.path.dirname(os.path.abspath("rotaryGuestBook.py")) + "/sounds/927.wav") - config['playback_reduction']) | ||||
|  | ||||
|     # elif dialed[0:4] == [5,4,5,3]: | ||||
|     #     # play special vm | ||||
|     #     play(AudioSegment.from_wav(os.path.dirname(os.path.abspath("rotaryGuestBook.py")) + "/sounds/beep.wav") - config['beep_reduction']) | ||||
|  | ||||
|  | ||||
| def onHook(): | ||||
|     print("Phone on hook. Sleeping...") | ||||
|     # print("Resetting dial list") | ||||
|     # global dialed | ||||
|     # dialed = [] | ||||
|     # reset_pulse_counter() | ||||
|  | ||||
|  | ||||
| # def dialing(): | ||||
| #     if hook.is_pressed: | ||||
| #         global count, reset_flag | ||||
| #         count+=1 | ||||
| #         print(f"dialing, increment count: {count}") | ||||
| #         reset_flag = False | ||||
|  | ||||
| # def reset_pulse_counter(): | ||||
| #     global count, reset_flag | ||||
| #     count = 0 | ||||
| #     print(f"reset count: {count}") | ||||
| #     reset_flag = True | ||||
|  | ||||
|  | ||||
| # def held(): | ||||
| #     if not reset_flag: | ||||
| #         print("holding") | ||||
| #         print(count) | ||||
| #         global dialed | ||||
| #         if (count == 10): | ||||
| #             dialed.append(0) | ||||
| #         else: | ||||
| #             dialed.append(count) | ||||
| #         print(f"number dialed: {dialed}") | ||||
| #         offHook() | ||||
| #         reset_pulse_counter() | ||||
|  | ||||
|  | ||||
| def main(): | ||||
|     # rotaryDial.when_pressed = dialing | ||||
|     # rotaryDial.when_held = held | ||||
|  | ||||
|     hook.when_pressed = offHook | ||||
|     hook.when_released = onHook | ||||
|     pause() | ||||
|  | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     main() | ||||
							
								
								
									
										
											BIN
										
									
								
								sounds/beep.wav
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										136
									
								
								todo/audioGuestBookwithRotaryDialer.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,136 @@ | ||||
| #! /usr/bin/env python3 | ||||
|  | ||||
| import audioInterface | ||||
| import os | ||||
| import yaml | ||||
| import sys | ||||
|  | ||||
| from datetime import datetime | ||||
| from gpiozero import Button | ||||
| from multiprocessing import Process | ||||
| from signal import pause | ||||
| from pydub import AudioSegment | ||||
| from pydub.playback import play | ||||
| from pydub.scipy_effects import band_pass_filter | ||||
| from pydub.effects import normalize, compress_dynamic_range | ||||
|  | ||||
| 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) | ||||
|  | ||||
| hook = Button(config["hook_gpio"]) | ||||
| rotaryDial = Button(pin=config['rotary_gpio'], hold_time=config['rotary_hold_time'], hold_repeat=config['rotary_hold_repeat']) | ||||
|  | ||||
| """ | ||||
| TODO: These globals are a temp solution for the rotary dialer, would love to not | ||||
| depend on globals for this logic. | ||||
| """ | ||||
| count = 0 | ||||
| dialed = [] | ||||
| reset_flag = False | ||||
|  | ||||
| def off_hook() -> None: | ||||
|     print("Phone off hook, ready to begin!") | ||||
|     # if dialed and dialed[0] == 0: | ||||
|     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" | ||||
|         ) | ||||
|         - config["playback_reduction"] | ||||
|     ) | ||||
|  | ||||
|     # 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"] | ||||
|     ) | ||||
|  | ||||
|     # now, while phone is not off the hook, record audio from the microphone | ||||
|     print("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!") | ||||
|  | ||||
|     """ | ||||
|     post processing | ||||
|     """ | ||||
|     print("spawn postProcessing thread") | ||||
|     Process(target=audio_interface.postProcess, args=(output_file,)).start() | ||||
|  | ||||
|     """ | ||||
|     rotary dialer special messages | ||||
|     """ | ||||
|     if dialed[0:3] == [9,2,7]: | ||||
|         # play special vm | ||||
|         play(AudioSegment.from_wav(os.path.dirname(os.path.abspath(config["source_file"])) + "/sounds/927.wav") - config['playback_reduction']) | ||||
|  | ||||
|     elif dialed[0:4] == [5,4,5,3]: | ||||
|         # play special vm | ||||
|         play(AudioSegment.from_wav(os.path.dirname(os.path.abspath(config["source_file"])) + "/sounds/beep.wav") - config['beep_reduction']) | ||||
|  | ||||
|  | ||||
| def on_hook() -> None: | ||||
|     print("Phone on hook. Sleeping...") | ||||
|     print("Resetting dial list") | ||||
|     global dialed | ||||
|     dialed = [] | ||||
|     reset_pulse_counter() | ||||
|  | ||||
|  | ||||
| def dialing() -> None: | ||||
|     if hook.is_pressed: | ||||
|         global count, reset_flag | ||||
|         count+=1 | ||||
|         print(f"dialing, increment count: {count}") | ||||
|         reset_flag = False | ||||
|  | ||||
| def reset_pulse_counter() -> None: | ||||
|     global count, reset_flag | ||||
|     count = 0 | ||||
|     print(f"reset count: {count}") | ||||
|     reset_flag = True | ||||
|  | ||||
|  | ||||
| def held() -> None: | ||||
|     if not reset_flag: | ||||
|         print("holding") | ||||
|         print(count) | ||||
|         global dialed | ||||
|         if (count == 10): | ||||
|             dialed.append(0) | ||||
|         else: | ||||
|             dialed.append(count) | ||||
|         print(f"number dialed: {dialed}") | ||||
|         off_hook() | ||||
|         reset_pulse_counter() | ||||
|  | ||||
|  | ||||
| def main(): | ||||
|     rotaryDial.when_pressed = dialing | ||||
|     rotaryDial.when_held = held | ||||
|     hook.when_pressed = off_hook | ||||
|     hook.when_released = on_hook | ||||
|     pause() | ||||
|  | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     main() | ||||
 Craig Hesling
					Craig Hesling