commit 80c45389b2668924a29a36a32be3f65dfa058464 Author: grabowski Date: Fri Oct 24 14:37:20 2025 +0700 Add rotary phone web interface with multiple greeting support 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 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 0000000..6f9c1dc Binary files /dev/null and b/__pycache__/rotary_phone_web.cpython-311.pyc differ diff --git a/configure_hifiberry.sh b/configure_hifiberry.sh new file mode 100644 index 0000000..428d91f --- /dev/null +++ b/configure_hifiberry.sh @@ -0,0 +1,100 @@ +#!/bin/bash +# Auto-detect HiFiBerry and configure ALSA + +echo "==========================================" +echo "HiFiBerry Auto-Configuration" +echo "==========================================" +echo "" + +# Find HiFiBerry card number +echo "Detecting HiFiBerry..." +CARD_INFO=$(aplay -l | grep -i hifiberry) + +if [ -z "$CARD_INFO" ]; then + echo "āŒ HiFiBerry not found!" + echo "" + echo "Make sure:" + echo "1. HiFiBerry is properly seated on GPIO pins" + echo "2. /boot/firmware/config.txt has: dtoverlay=hifiberry-dacplusadcpro" + echo "3. You've rebooted after changing config.txt" + exit 1 +fi + +echo "āœ“ HiFiBerry found:" +echo "$CARD_INFO" +echo "" + +# Extract card number +CARD_NUM=$(echo "$CARD_INFO" | grep -oP 'card \K[0-9]+' | head -1) + +echo "Card number: $CARD_NUM" +echo "" + +# Create ALSA config +echo "Creating ~/.asoundrc..." +cat > ~/.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()