From 80c45389b2668924a29a36a32be3f65dfa058464 Mon Sep 17 00:00:00 2001 From: grabowski Date: Fri, 24 Oct 2025 14:37:20 +0700 Subject: [PATCH] Add rotary phone web interface with multiple greeting support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Features: - Web interface for managing rotary phone system - Support for multiple greeting messages with selector - Direct audio playback in browser for recordings and greetings - Upload multiple WAV files at once - Set active greeting that plays when phone is picked up - HiFiBerry DAC+ADC Pro audio configuration - GPIO-based handset detection and audio recording - Real-time status monitoring with auto-refresh šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .claude/settings.local.json | 10 + AUDIO_FIX.md | 166 +++ __pycache__/rotary_phone_web.cpython-311.pyc | Bin 0 -> 51238 bytes configure_hifiberry.sh | 100 ++ rotary_phone_web.py | 1228 ++++++++++++++++++ test_complete.py | 283 ++++ 6 files changed, 1787 insertions(+) create mode 100644 .claude/settings.local.json create mode 100644 AUDIO_FIX.md create mode 100644 __pycache__/rotary_phone_web.cpython-311.pyc create mode 100644 configure_hifiberry.sh create mode 100644 rotary_phone_web.py create mode 100644 test_complete.py diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..ded38bf --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,10 @@ +{ + "permissions": { + "allow": [ + "Bash(python -m py_compile:*)", + "Bash(git add:*)" + ], + "deny": [], + "ask": [] + } +} diff --git a/AUDIO_FIX.md b/AUDIO_FIX.md new file mode 100644 index 0000000..d6369fb --- /dev/null +++ b/AUDIO_FIX.md @@ -0,0 +1,166 @@ +# FIX: HiFiBerry Audio Working Now! šŸŽ‰ + +## The Problem + +Your HiFiBerry is detected as: +- **Card 3** (not card 0) +- **PyAudio device index 1** (not 0) + +The old configuration was trying to use device 0 (the built-in headphone jack), which is why you heard nothing from your speaker. + +## āœ… SOLUTION - Quick Fix + +### Method 1: Automatic Configuration (Recommended) + +Run this script to auto-detect and configure everything: + +```bash +cd /home/berwn +chmod +x configure_hifiberry.sh +./configure_hifiberry.sh +``` + +This will: +1. Auto-detect your HiFiBerry card number +2. Create the correct ~/.asoundrc +3. Set volume to 100% +4. Test the speaker + +### Method 2: Manual Configuration + +**Step 1: Create correct ALSA config** + +```bash +cat > ~/.asoundrc << 'EOF' +pcm.!default { + type asym + playback.pcm "plughw:3,0" + capture.pcm "plughw:3,0" +} + +ctl.!default { + type hw + card 3 +} +EOF +``` + +**Step 2: Set volume** + +```bash +amixer -c 3 sset Digital 100% +amixer -c 3 sset Analogue 100% +``` + +**Step 3: Test speaker** + +```bash +aplay -D plughw:3,0 /usr/share/sounds/alsa/Front_Center.wav +# OR +speaker-test -D plughw:3,0 -c 1 -t wav +``` + +### Method 3: Use the Updated Python Script + +The updated `rotary_phone_web.py` now uses device index 1 automatically! + +Just download the new version and run it: + +```bash +cd /home/berwn +python3 rotary_phone_web.py +``` + +## šŸŽµ Test Your Speaker Now + +After configuration, test with: + +```bash +# Using ALSA (command line) +aplay -D plughw:3,0 /usr/share/sounds/alsa/Front_Center.wav + +# Using speaker-test +speaker-test -D plughw:3,0 -c 1 -t sine -f 440 -l 3 + +# Using the Python script +python3 rotary_phone_web.py +# Then pick up the phone handset +``` + +## šŸ“ Understanding Your Audio Setup + +``` +Device 0: bcm2835 Headphones (built-in audio jack) +Device 1: HiFiBerry DAC+ADC Pro ← YOUR SPEAKER IS HERE! +``` + +**In ALSA terms:** +- Card 0 = bcm2835 (built-in) +- Card 3 = HiFiBerry ← YOUR CARD + +**In PyAudio terms:** +- Device index 0 = bcm2835 +- Device index 1 = HiFiBerry ← YOUR DEVICE + +## āœ… Updated Script Features + +The new `rotary_phone_web.py` includes: + +1. **Automatic device selection**: Uses device index 1 for HiFiBerry +2. **Both playback and recording**: Configured for your HiFiBerry +3. **Comments for easy changes**: If your device index differs + +If your HiFiBerry shows as a different device index, edit these lines in the script: + +```python +# Line ~95 - Playback +output_device_index=1, # Change this number if needed + +# Line ~135 - Recording +input_device_index=1, # Change this number if needed +``` + +## šŸ”§ Optional: Make HiFiBerry Card 0 + +If you want HiFiBerry to be card 0 (optional), disable the built-in audio: + +```bash +sudo nano /boot/firmware/config.txt +# (or /boot/config.txt on older systems) + +# Add or uncomment: +dtparam=audio=off + +# Make sure this exists: +dtoverlay=hifiberry-dacplusadcpro + +# Save and reboot +sudo reboot +``` + +After reboot, HiFiBerry will be card 0, and you can use the simpler config: + +```bash +cat > ~/.asoundrc << 'EOF' +pcm.!default { + type asym + playback.pcm "plughw:0,0" + capture.pcm "plughw:0,0" +} + +ctl.!default { + type hw + card 0 +} +EOF +``` + +## šŸŽ‰ You're All Set! + +Your speaker should now work perfectly with: +- āœ… Command-line tools (aplay, speaker-test) +- āœ… Python script (rotary_phone_web.py) +- āœ… Web interface +- āœ… Recording from microphone + +Enjoy your working rotary phone! šŸ“žšŸŽµ diff --git a/__pycache__/rotary_phone_web.cpython-311.pyc b/__pycache__/rotary_phone_web.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6f9c1dc12778b829361d86c26abf87822eb399eb GIT binary patch literal 51238 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 ~/.asoundrc << EOF +# ALSA Configuration for HiFiBerry DAC+ADC Pro +# Auto-generated configuration + +pcm.!default { + type asym + playback.pcm "plughw:${CARD_NUM},0" + capture.pcm "plughw:${CARD_NUM},0" +} + +ctl.!default { + type hw + card ${CARD_NUM} +} +EOF + +echo "āœ“ Created ~/.asoundrc with card ${CARD_NUM}" +echo "" + +# Show the config +echo "Configuration:" +cat ~/.asoundrc +echo "" + +# Set volume +echo "Setting volume to 100%..." +amixer -c ${CARD_NUM} sset Master 100% 2>/dev/null || echo "Master control not available" +amixer -c ${CARD_NUM} sset PCM 100% 2>/dev/null || echo "PCM control not available" +amixer -c ${CARD_NUM} sset Digital 100% 2>/dev/null || echo "Digital control not available" +amixer -c ${CARD_NUM} sset Analogue 100% 2>/dev/null || echo "Analogue control not available" +echo "" + +# Test speaker +echo "Testing speaker..." +echo "šŸ”Š Playing test tone - you should hear a beep!" +aplay -D plughw:${CARD_NUM},0 /usr/share/sounds/alsa/Front_Center.wav 2>/dev/null || \ +speaker-test -D plughw:${CARD_NUM},0 -c 1 -t sine -f 440 -l 2 2>/dev/null + +echo "" +echo "==========================================" +echo "Configuration Complete!" +echo "==========================================" +echo "" +echo "Your HiFiBerry is configured as card ${CARD_NUM}" +echo "" +echo "Test commands:" +echo " aplay -D plughw:${CARD_NUM},0 /usr/share/sounds/alsa/Front_Center.wav" +echo " speaker-test -D plughw:${CARD_NUM},0 -c 1 -t wav" +echo "" +echo "For Python script, use device index:" + +# Find PyAudio device index +python3 << PYEOF +import pyaudio +audio = pyaudio.PyAudio() +print("") +for i in range(audio.get_device_count()): + info = audio.get_device_info_by_index(i) + if 'hifiberry' in info['name'].lower(): + print(f" PyAudio device index: {i}") + print(f" Device name: {info['name']}") + break +audio.terminate() +PYEOF + +echo "" diff --git a/rotary_phone_web.py b/rotary_phone_web.py new file mode 100644 index 0000000..1cee28e --- /dev/null +++ b/rotary_phone_web.py @@ -0,0 +1,1228 @@ +#!/usr/bin/env python3 +""" +Rotary Phone Audio Handler with Web Interface +Detects handset pickup, plays custom sound, records audio, and provides web UI +""" + +import pyaudio +import wave +import time +import RPi.GPIO as GPIO +from datetime import datetime +import os +import numpy as np +import threading +from flask import Flask, render_template, request, send_file, jsonify, redirect, url_for +from werkzeug.utils import secure_filename +import json + +# Configuration +HOOK_PIN = 17 # GPIO pin for hook switch (change to your pin) +HOOK_PRESSED = GPIO.LOW # Change to GPIO.HIGH if your switch is active high + +# Audio settings +CHUNK = 1024 +FORMAT = pyaudio.paInt16 +CHANNELS = 1 +RATE = 44100 +RECORD_SECONDS = 300 # Maximum recording time (5 minutes) + +# Directories +BASE_DIR = "/home/berwn/rotary_phone_data" +OUTPUT_DIR = os.path.join(BASE_DIR, "recordings") +SOUNDS_DIR = os.path.join(BASE_DIR, "sounds") +CONFIG_FILE = os.path.join(BASE_DIR, "config.json") +DIALTONE_FILE = os.path.join(SOUNDS_DIR, "dialtone.wav") # Legacy default + +# Web server settings +WEB_PORT = 8080 + +# Flask app +app = Flask(__name__) +app.config['MAX_CONTENT_LENGTH'] = 50 * 1024 * 1024 # 50MB max file size + +class RotaryPhone: + def __init__(self): + self.audio = pyaudio.PyAudio() + self.recording = False + self.phone_status = "on_hook" + self.current_recording = None + + # Setup GPIO + GPIO.setmode(GPIO.BCM) + GPIO.setup(HOOK_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) + + # Create directories + os.makedirs(OUTPUT_DIR, exist_ok=True) + os.makedirs(SOUNDS_DIR, exist_ok=True) + + # Initialize configuration + self.config = self.load_config() + + # Generate default dial tone if none exists + if not os.path.exists(DIALTONE_FILE): + self.generate_default_dialtone() + + def load_config(self): + """Load configuration from JSON file""" + default_config = { + "active_greeting": "dialtone.wav", + "greetings": [] + } + + if os.path.exists(CONFIG_FILE): + try: + with open(CONFIG_FILE, 'r') as f: + return json.load(f) + except: + pass + + return default_config + + def save_config(self): + """Save configuration to JSON file""" + with open(CONFIG_FILE, 'w') as f: + json.dump(self.config, f, indent=2) + + def get_active_greeting_path(self): + """Get the full path to the active greeting file""" + active = self.config.get("active_greeting", "dialtone.wav") + return os.path.join(SOUNDS_DIR, active) + + def set_active_greeting(self, filename): + """Set which greeting message to play""" + self.config["active_greeting"] = filename + self.save_config() + + def generate_default_dialtone(self): + """Generate a classic dial tone (350Hz + 440Hz) and save as default""" + print("Generating default dial tone...") + duration = 3 + sample_rate = 44100 + t = np.linspace(0, duration, int(sample_rate * duration), False) + + # Generate two frequencies and combine them + tone1 = np.sin(2 * np.pi * 350 * t) + tone2 = np.sin(2 * np.pi * 440 * t) + tone = (tone1 + tone2) / 2 + + # Convert to 16-bit PCM + tone = (tone * 32767).astype(np.int16) + + # Save as WAV file + wf = wave.open(DIALTONE_FILE, 'wb') + wf.setnchannels(1) + wf.setsampwidth(2) # 16-bit + wf.setframerate(sample_rate) + wf.writeframes(tone.tobytes()) + wf.close() + print(f"Default dial tone saved to {DIALTONE_FILE}") + + def play_sound_file(self, filepath): + """Play a WAV file""" + if not os.path.exists(filepath): + print(f"Sound file not found: {filepath}") + return False + + print(f"Playing sound: {filepath}") + + try: + wf = wave.open(filepath, 'rb') + + # Use device index 1 for HiFiBerry (change if needed) + stream = self.audio.open( + format=self.audio.get_format_from_width(wf.getsampwidth()), + channels=wf.getnchannels(), + rate=wf.getframerate(), + output=True, + output_device_index=1, # HiFiBerry device index + frames_per_buffer=CHUNK + ) + + # Play the sound + data = wf.readframes(CHUNK) + while data and self.phone_status == "off_hook": + stream.write(data) + data = wf.readframes(CHUNK) + + stream.stop_stream() + stream.close() + wf.close() + print("Sound playback finished") + return True + + except Exception as e: + print(f"Error playing sound: {e}") + return False + + def record_audio(self): + """Record audio from the microphone""" + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + filename = os.path.join(OUTPUT_DIR, f"recording_{timestamp}.wav") + self.current_recording = filename + + print(f"Recording to {filename}") + + try: + # Use device index 1 for HiFiBerry (change if needed) + stream = self.audio.open( + format=FORMAT, + channels=CHANNELS, + rate=RATE, + input=True, + input_device_index=1, # HiFiBerry device index + frames_per_buffer=CHUNK + ) + + frames = [] + self.recording = True + + # Record until handset is hung up or max time reached + start_time = time.time() + while self.recording and (time.time() - start_time) < RECORD_SECONDS: + # Check if handset is still off hook + if GPIO.input(HOOK_PIN) != HOOK_PRESSED: + print("Handset hung up, stopping recording") + break + + try: + data = stream.read(CHUNK, exception_on_overflow=False) + frames.append(data) + except Exception as e: + print(f"Error reading audio: {e}") + break + + stream.stop_stream() + stream.close() + + # Save the recording + if frames: + wf = wave.open(filename, 'wb') + wf.setnchannels(CHANNELS) + wf.setsampwidth(self.audio.get_sample_size(FORMAT)) + wf.setframerate(RATE) + wf.writeframes(b''.join(frames)) + wf.close() + duration = len(frames) * CHUNK / RATE + print(f"Recording saved: {filename} ({duration:.1f}s)") + else: + print("No audio recorded") + + except Exception as e: + print(f"Recording error: {e}") + + self.recording = False + self.current_recording = None + + def get_status(self): + """Get current phone status""" + return { + "status": self.phone_status, + "recording": self.recording, + "current_recording": self.current_recording + } + + def phone_loop(self): + """Main phone handling loop""" + print("Rotary Phone System Started") + print(f"Hook pin: GPIO {HOOK_PIN}") + print(f"Recordings will be saved to: {OUTPUT_DIR}") + + try: + while True: + # Wait for handset pickup + if GPIO.input(HOOK_PIN) == HOOK_PRESSED and self.phone_status == "on_hook": + self.phone_status = "off_hook" + print("\n=== Handset picked up ===") + + # Play active greeting message + greeting_file = self.get_active_greeting_path() + self.play_sound_file(greeting_file) + + # Start recording + if self.phone_status == "off_hook": # Still off hook after sound + self.record_audio() + + self.phone_status = "on_hook" + + time.sleep(0.1) + + except KeyboardInterrupt: + print("\nShutting down phone system...") + finally: + self.cleanup() + + def cleanup(self): + """Clean up resources""" + self.audio.terminate() + GPIO.cleanup() + print("Cleanup complete") + +# Global phone instance +phone = RotaryPhone() + +# Flask Routes +@app.route('/') +def index(): + """Main page""" + recordings = get_recordings() + greetings = get_greetings() + status = phone.get_status() + active_greeting = phone.config.get("active_greeting", "dialtone.wav") + + return render_template('index.html', + recordings=recordings, + greetings=greetings, + active_greeting=active_greeting, + status=status) + +@app.route('/api/status') +def api_status(): + """API endpoint for phone status""" + return jsonify(phone.get_status()) + +@app.route('/api/recordings') +def api_recordings(): + """API endpoint for recordings list""" + return jsonify(get_recordings()) + +@app.route('/api/greetings') +def api_greetings(): + """API endpoint for greetings list""" + return jsonify(get_greetings()) + +@app.route('/upload_greeting', methods=['POST']) +def upload_greeting(): + """Upload a new greeting message""" + if 'soundfile' not in request.files: + return jsonify({"error": "No file provided"}), 400 + + file = request.files['soundfile'] + + if file.filename == '': + return jsonify({"error": "No file selected"}), 400 + + if file and file.filename.lower().endswith('.wav'): + filename = secure_filename(file.filename) + + # Ensure unique filename + counter = 1 + base_name = os.path.splitext(filename)[0] + while os.path.exists(os.path.join(SOUNDS_DIR, filename)): + filename = f"{base_name}_{counter}.wav" + counter += 1 + + filepath = os.path.join(SOUNDS_DIR, filename) + file.save(filepath) + + return jsonify({"success": True, "message": f"Greeting '{filename}' uploaded successfully", "filename": filename}) + + return jsonify({"error": "Only WAV files are supported"}), 400 + +@app.route('/set_active_greeting', methods=['POST']) +def set_active_greeting(): + """Set which greeting to play""" + data = request.get_json() + filename = data.get('filename') + + if not filename: + return jsonify({"error": "No filename provided"}), 400 + + filepath = os.path.join(SOUNDS_DIR, filename) + if not os.path.exists(filepath): + return jsonify({"error": "Greeting file not found"}), 404 + + phone.set_active_greeting(filename) + return jsonify({"success": True, "message": f"Active greeting set to '{filename}'"}) + +@app.route('/delete_greeting/', methods=['POST']) +def delete_greeting(filename): + """Delete a greeting file""" + filename = secure_filename(filename) + filepath = os.path.join(SOUNDS_DIR, filename) + + # Don't delete the active greeting + if filename == phone.config.get("active_greeting"): + return jsonify({"error": "Cannot delete the active greeting. Please select a different greeting first."}), 400 + + if os.path.exists(filepath): + os.remove(filepath) + return jsonify({"success": True}) + + return jsonify({"error": "File not found"}), 404 + +@app.route('/play_audio//') +def play_audio(audio_type, filename): + """Serve audio file for web playback""" + filename = secure_filename(filename) + + if audio_type == 'recording': + filepath = os.path.join(OUTPUT_DIR, filename) + elif audio_type == 'greeting': + filepath = os.path.join(SOUNDS_DIR, filename) + else: + return "Invalid audio type", 400 + + if os.path.exists(filepath): + return send_file(filepath, mimetype='audio/wav') + return "File not found", 404 + +@app.route('/download/') +def download_recording(filename): + """Download a recording""" + filepath = os.path.join(OUTPUT_DIR, secure_filename(filename)) + if os.path.exists(filepath): + return send_file(filepath, as_attachment=True) + return "File not found", 404 + +@app.route('/delete/', methods=['POST']) +def delete_recording(filename): + """Delete a recording""" + filepath = os.path.join(OUTPUT_DIR, secure_filename(filename)) + if os.path.exists(filepath): + os.remove(filepath) + return jsonify({"success": True}) + return jsonify({"error": "File not found"}), 404 + +@app.route('/restore_default_sound', methods=['POST']) +def restore_default_sound(): + """Restore default dial tone""" + if os.path.exists(DIALTONE_FILE): + os.remove(DIALTONE_FILE) + phone.generate_default_dialtone() + phone.set_active_greeting("dialtone.wav") + return jsonify({"success": True, "message": "Default dial tone restored"}) + +def get_greetings(): + """Get list of all greeting sound files""" + greetings = [] + if os.path.exists(SOUNDS_DIR): + for filename in sorted(os.listdir(SOUNDS_DIR)): + if filename.endswith('.wav'): + filepath = os.path.join(SOUNDS_DIR, filename) + stat = os.stat(filepath) + + # Get duration from WAV file + try: + wf = wave.open(filepath, 'rb') + frames = wf.getnframes() + rate = wf.getframerate() + duration = frames / float(rate) + wf.close() + except: + duration = 0 + + greetings.append({ + "filename": filename, + "size": stat.st_size, + "size_mb": stat.st_size / (1024 * 1024), + "date": datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d %H:%M:%S'), + "duration": duration, + "is_active": filename == phone.config.get("active_greeting", "dialtone.wav") + }) + return greetings + +def get_recordings(): + """Get list of all recordings with metadata""" + recordings = [] + if os.path.exists(OUTPUT_DIR): + for filename in sorted(os.listdir(OUTPUT_DIR), reverse=True): + if filename.endswith('.wav'): + filepath = os.path.join(OUTPUT_DIR, filename) + stat = os.stat(filepath) + + # Get duration from WAV file + try: + wf = wave.open(filepath, 'rb') + frames = wf.getnframes() + rate = wf.getframerate() + duration = frames / float(rate) + wf.close() + except: + duration = 0 + + recordings.append({ + "filename": filename, + "size": stat.st_size, + "size_mb": stat.st_size / (1024 * 1024), + "date": datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d %H:%M:%S'), + "duration": duration + }) + return recordings + +def get_local_ip(): + """Get local IP address""" + import socket + try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(("8.8.8.8", 80)) + ip = s.getsockname()[0] + s.close() + return ip + except: + return "127.0.0.1" + +if __name__ == "__main__": + # Create templates directory and HTML template + script_dir = os.path.dirname(os.path.abspath(__file__)) + templates_dir = os.path.join(script_dir, 'templates') + os.makedirs(templates_dir, exist_ok=True) + + # Create the HTML template if it doesn't exist + template_file = os.path.join(templates_dir, 'index.html') + if not os.path.exists(template_file): + print(f"Creating template at {template_file}") + with open(template_file, 'w') as f: + f.write(''' + + + + + Rotary Phone Control Panel + + + +
+
+

šŸ“ž Rotary Phone Control Panel

+

Manage your vintage phone system

+
+ +
+ + +
+
+

Phone Status

+
+
+ + {% if status.recording %} + šŸ”“ Recording in progress... + {% elif status.status == 'off_hook' %} + šŸ“ž Handset off hook + {% else %} + āœ… Ready (handset on hook) + {% endif %} + +
+ {% if status.current_recording %} +

+ Recording to: {{ status.current_recording.split('/')[-1] }} +

+ {% endif %} +
+ +
+ + +
+

šŸŽµ Greeting Messages

+ +
+ ā„¹ļø Active greeting: {{ active_greeting }} +
+ +
+

+ Upload WAV files to play when the handset is picked up +

+
+ + + +
+ +
+ + +
+
+ + + {% if greetings %} +

Available Greetings

+
+ {% for greeting in greetings %} +
+
+

+ {% if greeting.is_active %}⭐{% endif %} {{ greeting.filename }} +

+
+ šŸ“… {{ greeting.date }} | + ā±ļø {{ "%.1f"|format(greeting.duration) }}s | + šŸ’¾ {{ "%.2f"|format(greeting.size_mb) }} MB +
+
+
+ + {% if not greeting.is_active %} + + + {% else %} + + {% endif %} +
+
+ {% endfor %} +
+ {% else %} +
+

šŸŽµ

+

No greeting messages uploaded yet. Upload your first greeting!

+
+ {% endif %} +
+ + +
+

šŸŽ™ļø Recordings

+ + {% if recordings %} +
+
+
{{ recordings|length }}
+
Total Recordings
+
+
+
{{ "%.1f"|format(recordings|sum(attribute='size_mb')) }} MB
+
Total Size
+
+
+
{{ "%.1f"|format(recordings|sum(attribute='duration')/60) }} min
+
Total Duration
+
+
+ +
+ {% for recording in recordings %} +
+
+

{{ recording.filename }}

+
+ šŸ“… {{ recording.date }} | + ā±ļø {{ "%.1f"|format(recording.duration) }}s | + šŸ’¾ {{ "%.2f"|format(recording.size_mb) }} MB +
+
+
+ + + +
+
+ {% endfor %} +
+ {% else %} +
+

šŸ“­

+

No recordings yet. Pick up the phone to start recording!

+
+ {% endif %} +
+
+ + +
+
+
+

šŸŽµ Audio Player

+ +
+
+
+ +
+
+
+ + + +''') + else: + print(f"Template already exists at {template_file}") + + # Start phone handling in separate thread + phone_thread = threading.Thread(target=phone.phone_loop, daemon=True) + phone_thread.start() + + # Get and display local IP + local_ip = get_local_ip() + print("\n" + "="*60) + print(f"Web Interface Available At:") + print(f" http://{local_ip}:{WEB_PORT}") + print(f" http://localhost:{WEB_PORT}") + print("="*60 + "\n") + + # Start Flask web server + app.run(host='0.0.0.0', port=WEB_PORT, debug=False, threaded=True) diff --git a/test_complete.py b/test_complete.py new file mode 100644 index 0000000..657276a --- /dev/null +++ b/test_complete.py @@ -0,0 +1,283 @@ +#!/usr/bin/env python3 +""" +Complete HiFiBerry Test - Verify speaker and microphone work +""" + +import pyaudio +import wave +import numpy as np +import time +import os + +# HiFiBerry device index (from your system) +HIFIBERRY_INDEX = 1 +SAMPLE_RATE = 44100 + +def test_playback(): + """Test speaker output""" + print("\n" + "="*60) + print("šŸ”Š TESTING SPEAKER PLAYBACK") + print("="*60) + + audio = pyaudio.PyAudio() + + try: + # Show device info + info = audio.get_device_info_by_index(HIFIBERRY_INDEX) + print(f"\nUsing device: {info['name']}") + print(f"Max output channels: {info['maxOutputChannels']}") + + # Generate test tone (440Hz A note) + print("\nGenerating 440Hz test tone...") + duration = 3 + t = np.linspace(0, duration, int(SAMPLE_RATE * duration), False) + tone = np.sin(2 * np.pi * 440 * t) + tone = (tone * 0.3 * 32767).astype(np.int16) # 30% volume + + # Open stream + stream = audio.open( + format=pyaudio.paInt16, + channels=1, + rate=SAMPLE_RATE, + output=True, + output_device_index=HIFIBERRY_INDEX, + frames_per_buffer=1024 + ) + + print("šŸŽµ Playing 3-second tone - LISTEN NOW!") + print(" You should hear a clear beep from your speaker...") + + # Play + chunk_size = 2048 + for i in range(0, len(tone.tobytes()), chunk_size): + stream.write(tone.tobytes()[i:i + chunk_size]) + + stream.stop_stream() + stream.close() + + print("āœ“ Playback completed") + return True + + except Exception as e: + print(f"āŒ Playback error: {e}") + return False + finally: + audio.terminate() + +def test_recording(): + """Test microphone input""" + print("\n" + "="*60) + print("šŸŽ™ļø TESTING MICROPHONE RECORDING") + print("="*60) + + audio = pyaudio.PyAudio() + + try: + # Show device info + info = audio.get_device_info_by_index(HIFIBERRY_INDEX) + print(f"\nUsing device: {info['name']}") + print(f"Max input channels: {info['maxInputChannels']}") + + # Record + print("\nšŸ”“ Recording for 5 seconds...") + print(" SPEAK NOW or make noise near the microphone...") + + stream = audio.open( + format=pyaudio.paInt16, + channels=1, + rate=SAMPLE_RATE, + input=True, + input_device_index=HIFIBERRY_INDEX, + frames_per_buffer=1024 + ) + + frames = [] + for i in range(0, int(SAMPLE_RATE / 1024 * 5)): + data = stream.read(1024, exception_on_overflow=False) + frames.append(data) + + stream.stop_stream() + stream.close() + + print("āœ“ Recording completed") + + # Save to file + filename = "/tmp/test_recording.wav" + wf = wave.open(filename, 'wb') + wf.setnchannels(1) + wf.setsampwidth(audio.get_sample_size(pyaudio.paInt16)) + wf.setframerate(SAMPLE_RATE) + wf.writeframes(b''.join(frames)) + wf.close() + + print(f"āœ“ Saved to {filename}") + + # Calculate volume level + audio_data = np.frombuffer(b''.join(frames), dtype=np.int16) + volume = np.abs(audio_data).mean() + max_volume = np.abs(audio_data).max() + + print(f"\nRecording analysis:") + print(f" Average level: {volume:.0f}") + print(f" Peak level: {max_volume}") + + if max_volume > 1000: + print(" āœ“ Good signal detected!") + else: + print(" āš ļø Very low signal - microphone might not be working") + + audio.terminate() + + # Play back + print("\nšŸ”Š Playing back your recording...") + audio = pyaudio.PyAudio() + + wf = wave.open(filename, 'rb') + stream = audio.open( + format=audio.get_format_from_width(wf.getsampwidth()), + channels=wf.getnchannels(), + rate=wf.getframerate(), + output=True, + output_device_index=HIFIBERRY_INDEX + ) + + data = wf.readframes(1024) + while data: + stream.write(data) + data = wf.readframes(1024) + + stream.stop_stream() + stream.close() + wf.close() + + print("āœ“ Playback of recording completed") + print(f"\nYou can listen again with: aplay {filename}") + + return True + + except Exception as e: + print(f"āŒ Recording error: {e}") + return False + finally: + audio.terminate() + +def test_dial_tone(): + """Test classic dial tone (350Hz + 440Hz)""" + print("\n" + "="*60) + print("šŸ“ž TESTING DIAL TONE") + print("="*60) + + audio = pyaudio.PyAudio() + + try: + print("\nGenerating classic dial tone (350Hz + 440Hz)...") + duration = 3 + t = np.linspace(0, duration, int(SAMPLE_RATE * duration), False) + + # Generate two frequencies + tone1 = np.sin(2 * np.pi * 350 * t) + tone2 = np.sin(2 * np.pi * 440 * t) + tone = (tone1 + tone2) / 2 + tone = (tone * 0.3 * 32767).astype(np.int16) + + stream = audio.open( + format=pyaudio.paInt16, + channels=1, + rate=SAMPLE_RATE, + output=True, + output_device_index=HIFIBERRY_INDEX, + frames_per_buffer=1024 + ) + + print("šŸŽµ Playing dial tone - This is what you'll hear when picking up the phone!") + + chunk_size = 2048 + for i in range(0, len(tone.tobytes()), chunk_size): + stream.write(tone.tobytes()[i:i + chunk_size]) + + stream.stop_stream() + stream.close() + + print("āœ“ Dial tone playback completed") + return True + + except Exception as e: + print(f"āŒ Dial tone error: {e}") + return False + finally: + audio.terminate() + +def show_device_info(): + """Show all audio devices""" + print("\n" + "="*60) + print("šŸ“‹ AUDIO DEVICES ON YOUR SYSTEM") + print("="*60) + + audio = pyaudio.PyAudio() + + print("\nAll devices:") + for i in range(audio.get_device_count()): + info = audio.get_device_info_by_index(i) + device_type = [] + if info['maxOutputChannels'] > 0: + device_type.append("OUTPUT") + if info['maxInputChannels'] > 0: + device_type.append("INPUT") + + marker = " ← USING THIS" if i == HIFIBERRY_INDEX else "" + print(f" [{i}] {info['name']}{marker}") + print(f" Type: {', '.join(device_type)}") + print(f" Sample rate: {info['defaultSampleRate']}") + print() + + audio.terminate() + +def main(): + """Main test routine""" + print("\n" + "="*60) + print("šŸŽ›ļø HIFIBERRY COMPLETE SYSTEM TEST") + print("="*60) + print(f"\nTesting HiFiBerry at device index: {HIFIBERRY_INDEX}") + + # Show devices + show_device_info() + + input("\nPress Enter to start tests...") + + # Test 1: Playback + test1 = test_playback() + time.sleep(1) + + # Test 2: Dial tone + test2 = test_dial_tone() + time.sleep(1) + + # Test 3: Recording + test3 = test_recording() + + # Summary + print("\n" + "="*60) + print("šŸ“Š TEST SUMMARY") + print("="*60) + print(f"\nāœ“ Speaker playback: {'PASS āœ“' if test1 else 'FAIL āŒ'}") + print(f"āœ“ Dial tone: {'PASS āœ“' if test2 else 'FAIL āŒ'}") + print(f"āœ“ Microphone recording: {'PASS āœ“' if test3 else 'FAIL āŒ'}") + + if test1 and test2 and test3: + print("\nšŸŽ‰ ALL TESTS PASSED!") + print("\nYour rotary phone system is ready to use!") + print("Run: python3 rotary_phone_web.py") + else: + print("\nāš ļø Some tests failed. Check the output above for details.") + + print("\n" + "="*60) + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + print("\n\nTest interrupted by user") + except Exception as e: + print(f"\nāŒ Error: {e}") + import traceback + traceback.print_exc()