Compare commits
2 Commits
Author | SHA1 | Date | |
---|---|---|---|
4e06202786 | |||
47413a72cf |
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@ -1 +0,0 @@
|
|||||||
ko_fi: dillpicholas
|
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,3 +1,2 @@
|
|||||||
*.code*
|
*.code*
|
||||||
*.trunk*
|
*.trunk*
|
||||||
__pycache__
|
|
@ -163,7 +163,7 @@ To replace:
|
|||||||
- On the networked rpi - clone the repository:
|
- On the networked rpi - clone the repository:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone git@github.com:nickpourazima/rotary-phone-audio-guestbook.git
|
git clone https://git.izebra.net/izebra_projects/rotary-phone-audio-guestbook.git
|
||||||
cd rotary-phone-audio-guestbook
|
cd rotary-phone-audio-guestbook
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ import yaml
|
|||||||
from gpiozero import Button
|
from gpiozero import Button
|
||||||
from pydub import AudioSegment, playback
|
from pydub import AudioSegment, playback
|
||||||
|
|
||||||
import audioInterface as audioInterface
|
import audioInterface
|
||||||
|
|
||||||
# Set up logging
|
# Set up logging
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
@ -27,15 +27,6 @@ FORMATS = {
|
|||||||
|
|
||||||
|
|
||||||
def load_config():
|
def load_config():
|
||||||
"""
|
|
||||||
Loads the configuration from a YAML file.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: Configuration dictionary.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
SystemExit: If the configuration file is not found.
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
with CONFIG_PATH.open() as f:
|
with CONFIG_PATH.open() as f:
|
||||||
return yaml.safe_load(f)
|
return yaml.safe_load(f)
|
||||||
@ -47,13 +38,6 @@ def load_config():
|
|||||||
|
|
||||||
|
|
||||||
def play_audio(filename, reduction=0):
|
def play_audio(filename, reduction=0):
|
||||||
"""
|
|
||||||
Plays an audio file with the option to reduce its volume.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
filename (str): The name of the audio file to play.
|
|
||||||
reduction (int): The amount of volume reduction (default is 0).
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
sound_path = BASE_DIR / "sounds" / filename
|
sound_path = BASE_DIR / "sounds" / filename
|
||||||
sound = AudioSegment.from_wav(sound_path) - reduction
|
sound = AudioSegment.from_wav(sound_path) - reduction
|
||||||
@ -63,12 +47,6 @@ def play_audio(filename, reduction=0):
|
|||||||
|
|
||||||
|
|
||||||
def off_hook():
|
def off_hook():
|
||||||
"""
|
|
||||||
Handles the off-hook event.
|
|
||||||
|
|
||||||
Initializes the audio interface, plays the voicemail and beep sounds,
|
|
||||||
and starts recording the audio.
|
|
||||||
"""
|
|
||||||
global hook, config
|
global hook, config
|
||||||
|
|
||||||
logger.info("Phone off hook, ready to begin!")
|
logger.info("Phone off hook, ready to begin!")
|
||||||
@ -83,18 +61,14 @@ def off_hook():
|
|||||||
dev_index=config["alsa_hw_mapping"],
|
dev_index=config["alsa_hw_mapping"],
|
||||||
hook_type=config["hook_type"],
|
hook_type=config["hook_type"],
|
||||||
)
|
)
|
||||||
# Explicitly initialize audio resources
|
|
||||||
audio_interface.init_audio()
|
|
||||||
|
|
||||||
# Playing pre-recorded messages before recording
|
|
||||||
logger.info("Playing voicemail message...")
|
logger.info("Playing voicemail message...")
|
||||||
play_audio("voicemail.wav", config["playback_reduction"])
|
play_audio("voicemail.wav", config["playback_reduction"])
|
||||||
|
|
||||||
logger.info("Playing beep...")
|
logger.info("Playing beep...")
|
||||||
play_audio("beep.wav", config["beep_reduction"])
|
play_audio("beep.wav", config["beep_reduction"])
|
||||||
|
|
||||||
# Start recording
|
logger.info("recording")
|
||||||
logger.info("Recording")
|
|
||||||
audio_interface.record()
|
audio_interface.record()
|
||||||
audio_interface.stop()
|
audio_interface.stop()
|
||||||
|
|
||||||
@ -104,25 +78,12 @@ def off_hook():
|
|||||||
|
|
||||||
|
|
||||||
def on_hook():
|
def on_hook():
|
||||||
"""
|
|
||||||
Handles the on-hook event.
|
|
||||||
|
|
||||||
Logs a message indicating that the phone is on hook.
|
|
||||||
"""
|
|
||||||
logger.info("Phone on hook.\nSleeping...")
|
logger.info("Phone on hook.\nSleeping...")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""
|
|
||||||
The main function of the script.
|
|
||||||
|
|
||||||
Initializes the system, loads configuration, and sets up hook events.
|
|
||||||
"""
|
|
||||||
global config, hook
|
global config, hook
|
||||||
logger.info("Remember to monitor system resources during recording.")
|
|
||||||
config = load_config()
|
config = load_config()
|
||||||
|
|
||||||
# Setting up the hook based on configuration
|
|
||||||
if config["hook_type"] == "NC":
|
if config["hook_type"] == "NC":
|
||||||
hook = Button(config["hook_gpio"], pull_up=True)
|
hook = Button(config["hook_gpio"], pull_up=True)
|
||||||
hook.when_pressed = on_hook
|
hook.when_pressed = on_hook
|
@ -6,7 +6,7 @@ After=multi-user.target
|
|||||||
WorkingDirectory=<path-to-project>
|
WorkingDirectory=<path-to-project>
|
||||||
Type=simple
|
Type=simple
|
||||||
Restart=always
|
Restart=always
|
||||||
ExecStart=/usr/bin/env python3 <path-to-project>/src/audioGuestBook.py
|
ExecStart=/usr/bin/env python3 <path-to-project>/audioGuestBook.py
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
134
audioInterface.py
Normal file
134
audioInterface.py
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
#! /usr/bin/env python3
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
import wave
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
import pyaudio
|
||||||
|
from pydub import AudioSegment
|
||||||
|
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,
|
||||||
|
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 = 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):
|
||||||
|
self.init_audio()
|
||||||
|
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.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:
|
||||||
|
# Notify the user that their recording time is up
|
||||||
|
self.play("time_exceeded.wav")
|
||||||
|
break
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logger.info("Done recording")
|
||||||
|
except Exception as 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.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):
|
||||||
|
if self.stream:
|
||||||
|
self.stream.stop_stream()
|
||||||
|
self.stream.close()
|
||||||
|
|
||||||
|
if self.audio:
|
||||||
|
self.audio.terminate()
|
||||||
|
|
||||||
|
def close(self, output_file):
|
||||||
|
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 post_process(self, output_file):
|
||||||
|
source = AudioSegment.from_wav(output_file + ".wav")
|
||||||
|
filtered = self.filter_audio(source)
|
||||||
|
normalized = self.normalize_audio(filtered)
|
||||||
|
compressed = self.compress_audio(normalized)
|
||||||
|
|
||||||
|
normalized.export(output_file + "normalized.wav", format="wav")
|
||||||
|
compressed.export(output_file + "compressed.mp3", format="mp3")
|
||||||
|
|
||||||
|
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,5 +9,6 @@ rotary_gpio: 23
|
|||||||
rotary_hold_repeat: true
|
rotary_hold_repeat: true
|
||||||
rotary_hold_time: 0.25
|
rotary_hold_time: 0.25
|
||||||
sample_rate: 44100
|
sample_rate: 44100
|
||||||
|
source_file: audioGuestBook.py
|
||||||
format: INT16
|
format: INT16
|
||||||
hook_type: NC # or 'NO'
|
hook_type: NC # or 'NO'
|
2
docs/.gitignore
vendored
2
docs/.gitignore
vendored
@ -1,2 +0,0 @@
|
|||||||
_build
|
|
||||||
_static/images
|
|
@ -1,20 +0,0 @@
|
|||||||
# Minimal makefile for Sphinx documentation
|
|
||||||
#
|
|
||||||
|
|
||||||
# You can set these variables from the command line, and also
|
|
||||||
# from the environment for the first two.
|
|
||||||
SPHINXOPTS ?=
|
|
||||||
SPHINXBUILD ?= sphinx-build
|
|
||||||
SOURCEDIR = .
|
|
||||||
BUILDDIR = _build
|
|
||||||
|
|
||||||
# Put it first so that "make" without argument is like "make help".
|
|
||||||
help:
|
|
||||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
|
||||||
|
|
||||||
.PHONY: help Makefile
|
|
||||||
|
|
||||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
|
||||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
|
||||||
%: Makefile
|
|
||||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
|
548
docs/README.rst
548
docs/README.rst
@ -1,548 +0,0 @@
|
|||||||
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.).
|
|
||||||
|
|
||||||
.. figure:: _static/images/final_result_2.jpg
|
|
||||||
:alt: image
|
|
||||||
|
|
||||||
image
|
|
||||||
|
|
||||||
- `Rotary Phone Audio Guestbook <#rotary-phone-audio-guestbook>`__
|
|
||||||
|
|
||||||
- `Background <#background>`__
|
|
||||||
- `Post-Event Reflection <#post-event-reflection>`__
|
|
||||||
- `Future Enhancements <#future-enhancements>`__
|
|
||||||
- `Quick-Start <#quick-start>`__
|
|
||||||
- `Materials <#materials>`__
|
|
||||||
- `Hardware <#hardware>`__
|
|
||||||
|
|
||||||
- `Wiring <#wiring>`__
|
|
||||||
|
|
||||||
- `Hook <#hook>`__
|
|
||||||
- `Phone Cord <#phone-cord>`__
|
|
||||||
|
|
||||||
- `Optional: Microphone
|
|
||||||
Replacement <#optional-microphone-replacement>`__
|
|
||||||
|
|
||||||
- `Software <#software>`__
|
|
||||||
|
|
||||||
- `Dev Environment <#dev-environment>`__
|
|
||||||
- `Installation <#installation>`__
|
|
||||||
- `audioGuestBook systemctl
|
|
||||||
service <#audioguestbook-systemctl-service>`__
|
|
||||||
- `Config <#config>`__
|
|
||||||
- `AudioInterface Class <#audiointerface-class>`__
|
|
||||||
- `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>`__
|
|
||||||
|
|
||||||
Background
|
|
||||||
----------
|
|
||||||
|
|
||||||
Inspired by my own upcoming wedding, I created a DIY solution for an
|
|
||||||
audio guestbook using a rotary phone. With most online rentals charging
|
|
||||||
exorbitant fees without offering custom voicemail options, I sought a
|
|
||||||
more affordable and customizable solution. Here, I’ve detailed a guide
|
|
||||||
on creating your own audio guestbook. If you have questions, don’t
|
|
||||||
hesitate to reach out.
|
|
||||||
|
|
||||||
Post-Event Reflection
|
|
||||||
---------------------
|
|
||||||
|
|
||||||
The real event provided insights into areas of improvement for the
|
|
||||||
setup. For instance, introducing a recording time limit became essential
|
|
||||||
after some younger attendees left lengthy messages, draining the
|
|
||||||
battery. Depending on the situation, you might also consider connecting
|
|
||||||
the setup directly to a 5V power supply.
|
|
||||||
|
|
||||||
Future Enhancements
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
In anticipation of my wedding, I had code in place to detect dialed
|
|
||||||
numbers from the rotary encoder, allowing us to play special messages
|
|
||||||
for specific guests based on their dialed combination. However, this
|
|
||||||
required users to dial zero before leaving a voice message, introducing
|
|
||||||
an extra step. We opted for simplicity, but if you’re interested in
|
|
||||||
expanding on this, you’re welcome to explore further. The details of
|
|
||||||
this operation mode are described in `Mode
|
|
||||||
2 <#operation-mode-2-audioguestbookwithrotarydialer>`__
|
|
||||||
|
|
||||||
Additionally, threading the audio playback would be beneficial, allowing
|
|
||||||
for a watchdog service to terminate the thread upon a hook callback.
|
|
||||||
This would stop the message playback when a user hangs up.
|
|
||||||
|
|
||||||
Quick-Start
|
|
||||||
-----------
|
|
||||||
|
|
||||||
After cloning the repo on the rpi:
|
|
||||||
|
|
||||||
.. code:: bash
|
|
||||||
|
|
||||||
chmod +x installer.sh
|
|
||||||
./installer.sh
|
|
||||||
|
|
||||||
Materials
|
|
||||||
---------
|
|
||||||
|
|
||||||
.. raw:: html
|
|
||||||
|
|
||||||
<details>
|
|
||||||
|
|
||||||
Parts List
|
|
||||||
|
|
||||||
+------------------------------------------+------------------------+---+---+
|
|
||||||
| Part | Notes | Q | C |
|
|
||||||
| | | u | o |
|
|
||||||
| | | a | s |
|
|
||||||
| | | n | t |
|
|
||||||
| | | t | |
|
|
||||||
| | | i | |
|
|
||||||
| | | t | |
|
|
||||||
| | | y | |
|
|
||||||
+==========================================+========================+===+===+
|
|
||||||
| `rotary | Estate/garage/yard | 1 | $ |
|
|
||||||
| phone <https://www.ebay.com/b/Rot | sales are probably the | | 0 |
|
|
||||||
| ary-Dial-Telephone/38038/bn_55192308>`__ | best places to find | | . |
|
|
||||||
| | once of these. Ideally | | 0 |
|
|
||||||
| | one with a phone jack | | 0 |
|
|
||||||
| | since we will be using | | - |
|
|
||||||
| | these four wires | | $ |
|
|
||||||
| | extensively. | | 6 |
|
|
||||||
| | | | 0 |
|
|
||||||
| | | | . |
|
|
||||||
| | | | 0 |
|
|
||||||
| | | | 0 |
|
|
||||||
+------------------------------------------+------------------------+---+---+
|
|
||||||
| `raspberry pi | I didn’t realize how | 1 | $ |
|
|
||||||
| zero <https://www.raspber | hard these are to find | | 9 |
|
|
||||||
| rypi.com/products/raspberry-pi-zero/>`__ | these days. You can | | . |
|
|
||||||
| | use any rpi or arduino | | 9 |
|
|
||||||
| | style single-board | | 9 |
|
|
||||||
| | computer but be aware | | |
|
|
||||||
| | of size constraints | | |
|
|
||||||
| | (i.e. must fit inside | | |
|
|
||||||
| | the rotary phone | | |
|
|
||||||
| | enclosure) | | |
|
|
||||||
+------------------------------------------+------------------------+---+---+
|
|
||||||
| `raspberry pi zero | Optional: added for | 1 | $ |
|
|
||||||
| case <h | protection. One of the | | 4 |
|
|
||||||
| ttps://www.adafruit.com/product/3252>`__ | cases on Amazon has a | | . |
|
|
||||||
| | heat-sink cutout which | | 9 |
|
|
||||||
| | might be nice for | | 5 |
|
|
||||||
| | better heat | | |
|
|
||||||
| | dissapation since it | | |
|
|
||||||
| | will all be enclosed | | |
|
|
||||||
| | in the end. | | |
|
|
||||||
+------------------------------------------+------------------------+---+---+
|
|
||||||
| `micro SD | Any high | 1 | $ |
|
|
||||||
| card <https://a.co/d/1gb2zhC>`__ | capacity/throughput | | 8 |
|
|
||||||
| | micro SD card that is | | . |
|
|
||||||
| | rpi compatible | | 9 |
|
|
||||||
| | | | 9 |
|
|
||||||
+------------------------------------------+------------------------+---+---+
|
|
||||||
| `USB Audio | Note: I removed the | 1 | $ |
|
|
||||||
| Adapter <h | external plastic shell | | 4 |
|
|
||||||
| ttps://www.adafruit.com/product/1475>`__ | and directly soldered | | . |
|
|
||||||
| | the wires instead of | | 9 |
|
|
||||||
| | using the female 3.5mm | | 5 |
|
|
||||||
| | receptacle. | | |
|
|
||||||
+------------------------------------------+------------------------+---+---+
|
|
||||||
| `USB OTG Host Cable - MicroB OTG male to | | 1 | $ |
|
|
||||||
| A | | | 2 |
|
|
||||||
| female <h | | | . |
|
|
||||||
| ttps://www.adafruit.com/product/1099>`__ | | | 5 |
|
|
||||||
| | | | 0 |
|
|
||||||
+------------------------------------------+------------------------+---+---+
|
|
||||||
| — | **— If you don’t want | — | — |
|
|
||||||
| | to solder anything —** | | |
|
|
||||||
+------------------------------------------+------------------------+---+---+
|
|
||||||
| `3.5mm Male to Screw Terminal | Optional: can connect | 2 | $ |
|
|
||||||
| Connector <https://www | the handset cables | | 1 |
|
|
||||||
| .parts-express.com/3.5mm-Male-to-Screw-T | directly to the USB | | . |
|
|
||||||
| erminal-Connector-090-110?quantity=1&utm | audio interface via | | 3 |
|
|
||||||
| _source=google&utm_medium=cpc&utm_campai | these screw terminals | | 7 |
|
|
||||||
| gn=18395892906&utm_content=145242146127& | | | |
|
|
||||||
| gadid=623430178298&gclid=CjwKCAiAioifBhA | | | |
|
|
||||||
| XEiwApzCztl7aVb18WP4hDxnlQUCHsb62oIcnduF | | | |
|
|
||||||
| CSCbn9LFkZovYTQdr6omb3RoCD_gQAvD_BwE>`__ | | | |
|
|
||||||
+------------------------------------------+------------------------+---+---+
|
|
||||||
| — | **— If running off a | — | — |
|
|
||||||
| | battery —** | | |
|
|
||||||
+------------------------------------------+------------------------+---+---+
|
|
||||||
| `LiPo | Optional: maximize | 1 | $ |
|
|
||||||
| Battery <h | capacity based on what | | 1 |
|
|
||||||
| ttps://www.adafruit.com/product/2011>`__ | will fit within your | | 2 |
|
|
||||||
| | rotary enclosure. | | . |
|
|
||||||
| | | | 5 |
|
|
||||||
| | | | 0 |
|
|
||||||
+------------------------------------------+------------------------+---+---+
|
|
||||||
| `LiPo | Optional: if you plan | 1 | $ |
|
|
||||||
| Shim <h | to run this off a LiPo | | 9 |
|
|
||||||
| ttps://www.adafruit.com/product/3196>`__ | I would recommend | | . |
|
|
||||||
| | something like this to | | 9 |
|
|
||||||
| | interface with the rpi | | 5 |
|
|
||||||
| | zero. | | |
|
|
||||||
+------------------------------------------+------------------------+---+---+
|
|
||||||
| `LiPo | Optional: for | 1 | $ |
|
|
||||||
| Charger <h | re-charging the LiPo. | | 6 |
|
|
||||||
| ttps://www.adafruit.com/product/1904>`__ | | | . |
|
|
||||||
| | | | 9 |
|
|
||||||
| | | | 5 |
|
|
||||||
+------------------------------------------+------------------------+---+---+
|
|
||||||
| — | **— If replacing the | — | — |
|
|
||||||
| | built-it microphone | | |
|
|
||||||
| | —** | | |
|
|
||||||
+------------------------------------------+------------------------+---+---+
|
|
||||||
| `LavMic <https://www | Optional: if you’d | 1 | $ |
|
|
||||||
| .amazon.com/dp/B01N6P80OQ?ref=nb_sb_ss_w | like to replace the | | 2 |
|
|
||||||
| _as-reorder-t1_ypp_rep_k3_1_9&=&crid= | carbon microphone. | | 4 |
|
|
||||||
| 15WZEWMZ17EM9&=&sprefix=saramonic>`__ | This is an | | . |
|
|
||||||
| | omnidirectional | | 9 |
|
|
||||||
| | lavalier mic and | | 5 |
|
|
||||||
| | outputs via a 3.5mm | | |
|
|
||||||
| | TRS | | |
|
|
||||||
+------------------------------------------+------------------------+---+---+
|
|
||||||
|
|
||||||
.. raw:: html
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
Hardware
|
|
||||||
--------
|
|
||||||
|
|
||||||
Wiring
|
|
||||||
~~~~~~
|
|
||||||
|
|
||||||
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
|
|
||||||
==================================== ===========================
|
|
||||||
|hook1| |hook2|
|
|
||||||
==================================== ===========================
|
|
||||||
|
|
||||||
- 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.
|
|
||||||
|
|
||||||
.. figure:: _static/images/pi_block_terminal_wiring.jpg
|
|
||||||
:alt: image
|
|
||||||
|
|
||||||
image
|
|
||||||
|
|
||||||
- *Note: the green wire was used for the experimental rotary encoder
|
|
||||||
feature identified in the*\ `future
|
|
||||||
work <#future-enhancements>`__\ *section.*
|
|
||||||
|
|
||||||
=========================== =============
|
|
||||||
Rotary Phone Block Terminal Top-down view
|
|
||||||
=========================== =============
|
|
||||||
|term1| |term2|
|
|
||||||
=========================== =============
|
|
||||||
|
|
||||||
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*
|
|
||||||
|
|
||||||
.. figure:: _static/images/dissected_view_1.jpg
|
|
||||||
:alt: image
|
|
||||||
|
|
||||||
image
|
|
||||||
|
|
||||||
- Use this ALSA command from the command line to test if the mic is
|
|
||||||
working on the rpi before you set up the rotary phone: ``aplay -l``
|
|
||||||
|
|
||||||
- You might have a different hardware mapping than I did, in which
|
|
||||||
case you would change the ``alsa_hw_mapping`` in the
|
|
||||||
`config.yaml <config.yaml>`__.
|
|
||||||
- `Here’s <https://superuser.com/questions/53957/what-do-alsa-devices-like-hw0-0-mean-how-do-i-figure-out-which-to-use>`__
|
|
||||||
a good reference to device selection.
|
|
||||||
- You can also check
|
|
||||||
`this <https://stackoverflow.com/questions/32838279/getting-list-of-audio-input-devices-in-python>`__
|
|
||||||
from Python.
|
|
||||||
|
|
||||||
Optional: Microphone Replacement
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
For improved sound quality, consider replacing the built-in `carbon
|
|
||||||
microphone <https://en.wikipedia.org/wiki/Carbon_microphone>`__.
|
|
||||||
|
|
||||||
I found the sound quality of the built-in mic 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
|
|
||||||
- Prepare your lav mic
|
|
||||||
|
|
||||||
- I pulled off the 3.5mm male headphone pin since it is usually
|
|
||||||
coated and annoyingly difficult to solder directly on to.
|
|
||||||
- Carefully separate the two wires from the lav mic and spiral up
|
|
||||||
the surrounding copper. This will act as our ground signal.
|
|
||||||
|
|
||||||
- Extend the green wire from the phone cord clip to the ground point of
|
|
||||||
the lav mic.
|
|
||||||
- Red to red, black to blue as per the following diagram:
|
|
||||||
|
|
||||||
.. figure:: _static/images/phone_wiring.jpg
|
|
||||||
:alt: image
|
|
||||||
|
|
||||||
image
|
|
||||||
|
|
||||||
.. figure:: _static/images/handset_mic_wiring.jpg
|
|
||||||
:alt: image
|
|
||||||
|
|
||||||
image
|
|
||||||
|
|
||||||
.. figure:: _static/images/handset_mic_positioning.jpg
|
|
||||||
:alt: image
|
|
||||||
|
|
||||||
image
|
|
||||||
|
|
||||||
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
|
|
||||||
- *Optional: 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.
|
|
||||||
|
|
||||||
Installation
|
|
||||||
~~~~~~~~~~~~
|
|
||||||
|
|
||||||
- On the networked rpi - clone the repository:
|
|
||||||
|
|
||||||
.. code:: bash
|
|
||||||
|
|
||||||
git clone git@github.com:nickpourazima/rotary-phone-audio-guestbook.git
|
|
||||||
cd rotary-phone-audio-guestbook
|
|
||||||
|
|
||||||
- Next, use the installer script for a hassle-free setup.:
|
|
||||||
|
|
||||||
.. code:: bash
|
|
||||||
|
|
||||||
chmod +x installer.sh
|
|
||||||
./installer.sh
|
|
||||||
|
|
||||||
- Note, this script takes care of several tasks:
|
|
||||||
|
|
||||||
1. Install required dependencies.
|
|
||||||
2. Populate config.yaml based on user input
|
|
||||||
3. Replace placeholders in the service file to adapt to your project
|
|
||||||
directory.
|
|
||||||
4. Move the modified service file to the systemd directory.
|
|
||||||
5. Create necessary directories (recordings and sounds).
|
|
||||||
6. Grant execution permissions to the Python scripts.
|
|
||||||
7. Reload systemd, enable, and start the service.
|
|
||||||
|
|
||||||
`audioGuestBook systemctl service <audioGuestBook.service>`__
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
This service ensures smooth operation without manual intervention every
|
|
||||||
time your Raspberry Pi boots up. The installer script will place this
|
|
||||||
service file in the ``/etc/systemd/system`` directory and modify paths
|
|
||||||
according to your project directory.
|
|
||||||
|
|
||||||
Manual control of the service is possible as it operates as any other
|
|
||||||
```.service``
|
|
||||||
entity <https://www.freedesktop.org/software/systemd/man/systemd.service.html>`__
|
|
||||||
|
|
||||||
`Config <config.yaml>`__
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
- This file allows you to customize your own set up (edit rpi pins,
|
|
||||||
audio reduction, alsa mapping, etc), modify the yaml as necessary.
|
|
||||||
- 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:
|
|
||||||
|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.
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
- Off hook (released)
|
|
||||||
|
|
||||||
- Plays back your own added welcome message located in
|
|
||||||
``/sounds/voicemail.wav`` followed by the
|
|
||||||
`beep </sounds/beep.wav>`__ indicating the start of recording.
|
|
||||||
- Begins recording the guests voice message.
|
|
||||||
- Guest hangs up, recording is stopped and stored to the
|
|
||||||
``/recordings/`` directory.
|
|
||||||
- If the guest exceeds the **recording_limit** specified in the
|
|
||||||
`config.yaml </config.yaml>`__, play the warning
|
|
||||||
`time_exceeded.wav </sounds/time_exceeded.wav>`__ sound and stop
|
|
||||||
recording.
|
|
||||||
|
|
||||||
Operation Mode 2: `audioGuestBookwithRotaryDialer <./todo/audioGuestBookwithRotaryDialer.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.
|
|
||||||
- The rotary dialer is a bit more complex to set up, you need a pull up
|
|
||||||
resistor connected between the F screw terminal and 5V on the rpi and
|
|
||||||
the other end on GPIO 23. #TODO: Diagram
|
|
||||||
|
|
||||||
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
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
Verify the available sound devices using the following command:
|
|
||||||
|
|
||||||
.. code:: bash
|
|
||||||
|
|
||||||
aplay -l
|
|
||||||
|
|
||||||
*Ensure that your USB audio interface is listed and note the card and
|
|
||||||
device numbers.*
|
|
||||||
|
|
||||||
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:
|
|
||||||
|
|
||||||
.. code:: bash
|
|
||||||
|
|
||||||
defaults.pcm.card X
|
|
||||||
defaults.ctl.card X
|
|
||||||
|
|
||||||
*Replace X with the card number of your USB audio interface obtained
|
|
||||||
from the previous step.*
|
|
||||||
|
|
||||||
Restart ALSA
|
|
||||||
^^^^^^^^^^^^
|
|
||||||
|
|
||||||
.. code:: bash
|
|
||||||
|
|
||||||
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>`__! Feel like saying
|
|
||||||
thanks? You can `buy me a coffee <https://ko-fi.com/dillpicholas>`__\ ☕.
|
|
||||||
|
|
||||||
.. |hook1| image:: _static/images/hook_test_1.jpg
|
|
||||||
.. |hook2| image:: _static/images/hook_test_2.jpg
|
|
||||||
.. |term1| image:: _static/images/block_terminal.jpg
|
|
||||||
.. |term2| image:: _static/images/top_view_block_terminal.jpg
|
|
||||||
.. |rpi| image:: _static/images/rpi_GPIO.png
|
|
31
docs/conf.py
31
docs/conf.py
@ -1,31 +0,0 @@
|
|||||||
import os
|
|
||||||
import sys
|
|
||||||
sys.path.insert(0, os.path.abspath('../'))
|
|
||||||
# Configuration file for the Sphinx documentation builder.
|
|
||||||
#
|
|
||||||
# For the full list of built-in configuration values, see the documentation:
|
|
||||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
|
||||||
|
|
||||||
# -- Project information -----------------------------------------------------
|
|
||||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
|
||||||
|
|
||||||
project = 'Rotary Phone Audio Guestbook'
|
|
||||||
copyright = '2023, Nick Pourazima'
|
|
||||||
author = 'Nick Pourazima'
|
|
||||||
release = 'v1.0.0'
|
|
||||||
|
|
||||||
# -- General configuration ---------------------------------------------------
|
|
||||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
|
||||||
|
|
||||||
extensions = ['sphinx.ext.autodoc']
|
|
||||||
autodoc_typehints = 'description'
|
|
||||||
templates_path = ['_templates']
|
|
||||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# -- Options for HTML output -------------------------------------------------
|
|
||||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
|
|
||||||
|
|
||||||
html_theme = 'furo'
|
|
||||||
html_static_path = ['_static']
|
|
@ -1,19 +0,0 @@
|
|||||||
Welcome to Rotary Phone Audio Guestbook's documentation!
|
|
||||||
========================================================
|
|
||||||
|
|
||||||
This documentation covers the Rotary Phone Audio Guestbook project.
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:maxdepth: 2
|
|
||||||
:caption: Contents:
|
|
||||||
|
|
||||||
README
|
|
||||||
module_audioGuestBook
|
|
||||||
module_audioInterface
|
|
||||||
|
|
||||||
Indices and tables
|
|
||||||
==================
|
|
||||||
|
|
||||||
* :ref:`genindex`
|
|
||||||
* :ref:`modindex`
|
|
||||||
* :ref:`search`
|
|
@ -1,35 +0,0 @@
|
|||||||
@ECHO OFF
|
|
||||||
|
|
||||||
pushd %~dp0
|
|
||||||
|
|
||||||
REM Command file for Sphinx documentation
|
|
||||||
|
|
||||||
if "%SPHINXBUILD%" == "" (
|
|
||||||
set SPHINXBUILD=sphinx-build
|
|
||||||
)
|
|
||||||
set SOURCEDIR=.
|
|
||||||
set BUILDDIR=_build
|
|
||||||
|
|
||||||
%SPHINXBUILD% >NUL 2>NUL
|
|
||||||
if errorlevel 9009 (
|
|
||||||
echo.
|
|
||||||
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
|
||||||
echo.installed, then set the SPHINXBUILD environment variable to point
|
|
||||||
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
|
||||||
echo.may add the Sphinx directory to PATH.
|
|
||||||
echo.
|
|
||||||
echo.If you don't have Sphinx installed, grab it from
|
|
||||||
echo.https://www.sphinx-doc.org/
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "" goto help
|
|
||||||
|
|
||||||
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
|
||||||
goto end
|
|
||||||
|
|
||||||
:help
|
|
||||||
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
|
||||||
|
|
||||||
:end
|
|
||||||
popd
|
|
@ -1,7 +0,0 @@
|
|||||||
Audio Guest Book Module
|
|
||||||
=======================
|
|
||||||
|
|
||||||
This module contains the main script for the Rotary Phone Audio Guestbook.
|
|
||||||
|
|
||||||
.. automodule:: src.audioGuestBook
|
|
||||||
:members:
|
|
@ -1,9 +0,0 @@
|
|||||||
Audio Interface Class
|
|
||||||
=====================
|
|
||||||
|
|
||||||
The `AudioInterface` class is responsible for handling audio recording and playback functionalities.
|
|
||||||
|
|
||||||
.. autoclass:: src.audioInterface.AudioInterface
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
93
installer.sh
93
installer.sh
@ -1,88 +1,18 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# Rotary Phone Audio Guestbook Installer
|
# Dependency installation
|
||||||
|
echo "Installing dependencies..."
|
||||||
echo "Starting the installation process..."
|
sudo apt-get update
|
||||||
|
if ! sudo apt-get install -y python3-pip python3-gpiozero; then
|
||||||
# Update and install system dependencies
|
echo "Failed to install system packages."
|
||||||
echo "Installing additional dependencies..."
|
|
||||||
sudo apt-get install -y python3-pip python3-venv python3-gpiozero ffmpeg || {
|
|
||||||
echo "Failed to install required system packages."
|
|
||||||
exit 1
|
exit 1
|
||||||
}
|
fi
|
||||||
|
|
||||||
# Set up Python virtual environment for project dependencies
|
# Use --user flag for pip installations
|
||||||
echo "Setting up Python virtual environment..."
|
if ! pip3 install --user pydub pyaudio PyYAML; then
|
||||||
python3 -m venv ~/rotary-phone-venv || {
|
echo "Failed to install Python packages."
|
||||||
echo "Failed to create Python virtual environment."
|
|
||||||
exit 1
|
exit 1
|
||||||
}
|
fi
|
||||||
source ~/rotary-phone-venv/bin/activate
|
|
||||||
|
|
||||||
# Install Python dependencies in the virtual environment
|
|
||||||
pip install pydub pyaudio PyYAML sounddevice || {
|
|
||||||
echo "Failed to install Python dependencies."
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Modify PulseAudio configuration for improved audio handling
|
|
||||||
echo "Configuring PulseAudio..."
|
|
||||||
sudo cp /etc/pulse/default.pa /etc/pulse/default.pa.backup
|
|
||||||
echo -e "default-fragments = 5\ndefault-fragment-size-msec = 2" | sudo tee -a /etc/pulse/default.pa
|
|
||||||
|
|
||||||
# Restart PulseAudio to apply changes
|
|
||||||
pulseaudio -k
|
|
||||||
pulseaudio --start
|
|
||||||
|
|
||||||
# Display available sound cards and devices
|
|
||||||
echo "Listing available sound cards and devices:"
|
|
||||||
aplay -l
|
|
||||||
|
|
||||||
# Prompt user for ALSA configuration values
|
|
||||||
echo "Configuring ALSA..."
|
|
||||||
read -p "Enter the card number for the default playback card (e.g., 0, 1): " playback_card
|
|
||||||
read -p "Enter the card number for the default capture card (e.g., 0, 1): " capture_card
|
|
||||||
read -p "Enter the default sample rate (e.g., 44100): " sample_rate
|
|
||||||
while ! [[ "$sample_rate" =~ ^[89][0-9]{3}$|^[1-9][0-9]{4}$|^[1][0-8][0-9]{4}$|192000$ ]]; do
|
|
||||||
echo "Invalid sample rate. Please enter a value between 8000 and 192000."
|
|
||||||
read -p "Enter the default sample rate (e.g., 44100): " sample_rate
|
|
||||||
done
|
|
||||||
|
|
||||||
read -p "Enter the bit depth (16, 24, 32): " bit_depth
|
|
||||||
while ! [[ "$bit_depth" =~ ^(16|24|32)$ ]]; do
|
|
||||||
echo "Invalid bit depth. Please choose from 16, 24, or 32."
|
|
||||||
read -p "Enter the bit depth (16, 24, 32): " bit_depth
|
|
||||||
done
|
|
||||||
|
|
||||||
# Write ALSA configuration
|
|
||||||
echo "Applying ALSA configuration..."
|
|
||||||
sudo tee /etc/asound.conf >/dev/null <<EOF
|
|
||||||
# Custom ALSA configuration for Rotary Phone Audio Guestbook
|
|
||||||
defaults.pcm.rate_converter "samplerate"
|
|
||||||
defaults.pcm.dmix.rate $sample_rate
|
|
||||||
defaults.pcm.dmix.format S$bit_depth
|
|
||||||
defaults.ctl.card $playback_card
|
|
||||||
defaults.pcm.card $playback_card
|
|
||||||
defaults.pcm.device 0
|
|
||||||
defaults.pcm.subdevice -1
|
|
||||||
defaults.pcm.nonblock 1
|
|
||||||
defaults.pcm.compat 0
|
|
||||||
pcm.!default {
|
|
||||||
type hw
|
|
||||||
card $playback_card
|
|
||||||
}
|
|
||||||
ctl.!default {
|
|
||||||
type hw
|
|
||||||
card $capture_card
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Test recording and playback functionality
|
|
||||||
echo "Testing recording and playback..."
|
|
||||||
arecord -D hw:$capture_card,0 -d 5 -f cd test-mic.wav && aplay test-mic.wav || {
|
|
||||||
echo "Test failed. Check your microphone and speaker setup."
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Get the directory of the currently executing script
|
# Get the directory of the currently executing script
|
||||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
@ -191,6 +121,7 @@ rotary_gpio: 23
|
|||||||
rotary_hold_repeat: true
|
rotary_hold_repeat: true
|
||||||
rotary_hold_time: 0.25
|
rotary_hold_time: 0.25
|
||||||
sample_rate: $sample_rate
|
sample_rate: $sample_rate
|
||||||
|
source_file: audioGuestBook.py
|
||||||
format: $format
|
format: $format
|
||||||
hook_type: $hook_type
|
hook_type: $hook_type
|
||||||
EOF
|
EOF
|
||||||
@ -214,7 +145,7 @@ if ! sudo mkdir -p "$DIR/recordings"; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Set execution permissions for the main script
|
# Set execution permissions for the main script
|
||||||
if ! sudo chmod +x "$DIR/src/audioGuestBook.py"; then
|
if ! sudo chmod +x "$DIR/audioGuestBook.py"; then
|
||||||
echo "Failed to set script permissions."
|
echo "Failed to set script permissions."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
@ -1,226 +0,0 @@
|
|||||||
#! /usr/bin/env python3
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import time
|
|
||||||
import wave
|
|
||||||
|
|
||||||
import pyaudio
|
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class AudioInterface:
|
|
||||||
"""
|
|
||||||
A class to handle audio recording and playback functionalities.
|
|
||||||
|
|
||||||
:param chunk: The size of each audio chunk to be read or written.
|
|
||||||
:type chunk: int
|
|
||||||
:param chans: Number of audio channels.
|
|
||||||
:type chans: int
|
|
||||||
:param format: The format of the audio, e.g., pyaudio.paInt16.
|
|
||||||
:type format: int
|
|
||||||
:param frames: List to store frame bytes of the recorded audio.
|
|
||||||
:type frames: List[bytes]
|
|
||||||
:param hook: GPIO Button object to detect on and off-hook events.
|
|
||||||
:type hook: Button object
|
|
||||||
:param samp_rate: The sample rate of the audio.
|
|
||||||
:type samp_rate: int
|
|
||||||
:param recording_limit: Maximum recording duration in seconds.
|
|
||||||
:type recording_limit: int
|
|
||||||
:param dev_index: Index of the audio device to use.
|
|
||||||
:type dev_index: int
|
|
||||||
:param hook_type: Type of the hook (NC - Normally Closed, NO - Normally Open).
|
|
||||||
:type hook_type: str
|
|
||||||
:param filter_low_freq: Lower frequency for band-pass filter.
|
|
||||||
:type filter_low_freq: int
|
|
||||||
:param filter_high_freq: Higher frequency for band-pass filter.
|
|
||||||
:type filter_high_freq: int
|
|
||||||
:param audio: PyAudio object for audio operations.
|
|
||||||
:type audio: PyAudio object
|
|
||||||
:param stream: Audio stream for recording or playback.
|
|
||||||
:type stream: Audio
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
hook,
|
|
||||||
buffer_size,
|
|
||||||
channels,
|
|
||||||
format,
|
|
||||||
sample_rate,
|
|
||||||
recording_limit,
|
|
||||||
dev_index,
|
|
||||||
hook_type,
|
|
||||||
filter_low_freq=300,
|
|
||||||
filter_high_freq=10000,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Initializes the audio interface with the specified configuration.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
hook: GPIO Button object for hook detection.
|
|
||||||
buffer_size (int): Size of each audio buffer chunk.
|
|
||||||
channels (int): Number of audio channels.
|
|
||||||
format (int): Audio format (e.g., pyaudio.paInt16).
|
|
||||||
sample_rate (int): Audio sample rate.
|
|
||||||
recording_limit (int): Maximum recording time in seconds.
|
|
||||||
dev_index (int): Index of the audio device.
|
|
||||||
hook_type (str): Type of the hook (NC or NO).
|
|
||||||
filter_low_freq (int): Lower frequency for band-pass filter.
|
|
||||||
filter_high_freq (int): Higher frequency for band-pass filter.
|
|
||||||
"""
|
|
||||||
# Audio configuration
|
|
||||||
self.chunk = buffer_size
|
|
||||||
self.chans = channels
|
|
||||||
self.format = format
|
|
||||||
self.frames = []
|
|
||||||
self.hook = hook
|
|
||||||
self.samp_rate = sample_rate
|
|
||||||
self.recording_limit = recording_limit
|
|
||||||
self.dev_index = dev_index
|
|
||||||
self.hook_type = hook_type
|
|
||||||
self.filter_low_freq = filter_low_freq
|
|
||||||
self.filter_high_freq = filter_high_freq
|
|
||||||
|
|
||||||
# Audio resources
|
|
||||||
self.audio = None
|
|
||||||
self.stream = None
|
|
||||||
logger.info(
|
|
||||||
f"Initializing Audio Interface with sample rate: {sample_rate}, format: {format}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def init_audio(self):
|
|
||||||
"""
|
|
||||||
Initializes (or reinitializes) the audio resources for recording.
|
|
||||||
Closes any existing stream and PyAudio instance before re-creating them.
|
|
||||||
"""
|
|
||||||
# Closing existing stream if open
|
|
||||||
if self.stream is not None:
|
|
||||||
self.stream.stop_stream()
|
|
||||||
self.stream.close()
|
|
||||||
self.stream = None
|
|
||||||
|
|
||||||
# Terminating existing PyAudio instance if it exists
|
|
||||||
if self.audio is not None:
|
|
||||||
self.audio.terminate()
|
|
||||||
|
|
||||||
# Creating new PyAudio instance and resetting frame list
|
|
||||||
self.audio = pyaudio.PyAudio()
|
|
||||||
self.frames = []
|
|
||||||
logger.info("Audio resources initialized.")
|
|
||||||
|
|
||||||
def record(self):
|
|
||||||
"""
|
|
||||||
Records audio until the off-hook condition is false or the recording limit is reached.
|
|
||||||
|
|
||||||
This method initializes the audio stream and reads audio chunks in a loop, appending them to the frame list.
|
|
||||||
If the recording time exceeds the set limit, a 'time exceeded' notification is played.
|
|
||||||
"""
|
|
||||||
self.init_audio()
|
|
||||||
logger.info("Audio stream initialized for recording.")
|
|
||||||
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.off_hook_condition():
|
|
||||||
if time.time() - start < self.recording_limit:
|
|
||||||
data = self.stream.read(self.chunk, exception_on_overflow=False)
|
|
||||||
self.frames.append(data)
|
|
||||||
else:
|
|
||||||
# Notify the user that their recording time is up
|
|
||||||
self.play("time_exceeded.wav")
|
|
||||||
break
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
logger.info("Done recording")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Recording error: {e}")
|
|
||||||
|
|
||||||
def off_hook_condition(self):
|
|
||||||
"""
|
|
||||||
Determines the off-hook condition based on the hook type.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if the off-hook condition is met, False otherwise.
|
|
||||||
"""
|
|
||||||
return (
|
|
||||||
not self.hook.is_pressed if self.hook_type == "NC" else self.hook.is_pressed
|
|
||||||
)
|
|
||||||
|
|
||||||
def play(self, file):
|
|
||||||
"""
|
|
||||||
Plays an audio file.
|
|
||||||
|
|
||||||
This method initializes the audio resources and plays the specified audio file.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
file (str): The path to the audio file to be played.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
FileNotFoundError: If the specified audio file does not exist.
|
|
||||||
wave.Error: If there is an error processing the wave file.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
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)
|
|
||||||
except FileNotFoundError:
|
|
||||||
logger.error(f"File not found: {file}")
|
|
||||||
except wave.Error as e:
|
|
||||||
logger.error(f"Wave error: {e}")
|
|
||||||
finally:
|
|
||||||
if self.stream:
|
|
||||||
self.stream.stop_stream()
|
|
||||||
self.stream.close()
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
"""
|
|
||||||
Stops the audio stream and terminates the PyAudio session.
|
|
||||||
|
|
||||||
This method is used to cleanly stop audio playback or recording and release resources.
|
|
||||||
"""
|
|
||||||
if self.stream:
|
|
||||||
logger.info("Stopping audio stream.")
|
|
||||||
self.stream.stop_stream()
|
|
||||||
self.stream.close()
|
|
||||||
if self.audio:
|
|
||||||
logger.info("Terminating PyAudio session.")
|
|
||||||
self.audio.terminate()
|
|
||||||
|
|
||||||
def close(self, output_file):
|
|
||||||
"""
|
|
||||||
Closes the audio interface and saves the recorded frames to a file.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
output_file (str): The path to the output file where the recording will be saved.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
OSError: If there is an error writing the audio data to the file.
|
|
||||||
"""
|
|
||||||
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))
|
|
||||||
logger.info(f"Recording saved to {output_file}")
|
|
||||||
except OSError as e:
|
|
||||||
logger.error(f"Error writing to file {output_file}. Error: {e}")
|
|
@ -1,74 +0,0 @@
|
|||||||
import logging
|
|
||||||
from pydub import AudioSegment
|
|
||||||
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 AudioProcessing:
|
|
||||||
|
|
||||||
def post_process(self, output_file):
|
|
||||||
"""
|
|
||||||
Applies post-processing to the recorded audio and saves the processed files.
|
|
||||||
|
|
||||||
The post-processing includes filtering, normalization, and dynamic range compression.
|
|
||||||
The processed audio is saved in both WAV and MP3 formats.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
output_file (str): The base path for the output files.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
Exception: If there is an error during post-processing.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
source = AudioSegment.from_wav(output_file + ".wav")
|
|
||||||
filtered = self.filter_audio(source)
|
|
||||||
normalized = self.normalize_audio(filtered)
|
|
||||||
compressed = self.compress_audio(normalized)
|
|
||||||
|
|
||||||
normalized.export(output_file + "normalized.wav", format="wav")
|
|
||||||
compressed.export(output_file + "compressed.mp3", format="mp3")
|
|
||||||
logger.info("Post-processing completed successfully.")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Post-processing error: {e}")
|
|
||||||
|
|
||||||
def filter_audio(self, audio):
|
|
||||||
"""
|
|
||||||
Applies a band-pass filter to the given audio.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
audio (AudioSegment): The audio segment to be filtered.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
AudioSegment: The filtered audio segment.
|
|
||||||
"""
|
|
||||||
logger.info("Filtering audio.")
|
|
||||||
return band_pass_filter(audio, self.filter_low_freq, self.filter_high_freq)
|
|
||||||
|
|
||||||
def normalize_audio(self, audio):
|
|
||||||
"""
|
|
||||||
Normalizes the given audio segment.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
audio (AudioSegment): The audio segment to be normalized.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
AudioSegment: The normalized audio segment.
|
|
||||||
"""
|
|
||||||
logger.info("Normalizing audio.")
|
|
||||||
return normalize(audio)
|
|
||||||
|
|
||||||
def compress_audio(self, audio):
|
|
||||||
"""
|
|
||||||
Compresses the dynamic range of the given audio segment.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
audio (AudioSegment): The audio segment to be compressed.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
AudioSegment: The audio segment with compressed dynamic range.
|
|
||||||
"""
|
|
||||||
logger.info("Compressing dynamic range of audio.")
|
|
||||||
return compress_dynamic_range(audio)
|
|
@ -1,6 +1,6 @@
|
|||||||
#! /usr/bin/env python3
|
#! /usr/bin/env python3
|
||||||
|
|
||||||
import src.audioInterface as audioInterface
|
import audioInterface
|
||||||
import os
|
import os
|
||||||
import yaml
|
import yaml
|
||||||
import sys
|
import sys
|
Loading…
Reference in New Issue
Block a user