diff --git a/Makefile b/Makefile index 465b9f6..5c515ca 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ test: sync: @echo "Installing/syncing dependencies..." - uv pip install flask numpy pyaudio RPi.GPIO waitress + uv pip install flask numpy pyaudio RPi.GPIO waitress scipy install: sync @echo "Dependencies installed!" diff --git a/README.md b/README.md index 5e78b40..bb80a70 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,7 @@ Web interface available at: `http://:8080` - pyaudio>=0.2.13 - RPi.GPIO>=0.7.1 - waitress>=2.1.0 (production WSGI server) + - scipy>=1.7.0 (audio resampling utility) ## Installation @@ -506,7 +507,7 @@ The `config.json` file contains all system settings: "chunk_size": 1024, // Audio buffer size "format": "paInt16", // Audio format (16-bit) "channels": 1, // Mono audio - "sample_rate": 44100, // 44.1kHz sample rate + "sample_rate": 48000, // 48kHz sample rate "max_record_seconds": 300 // Max recording time (5 minutes) }, "paths": { @@ -527,6 +528,8 @@ The `config.json` file contains all system settings: "system": { "active_greeting": "dialtone.wav", // Default greeting "extra_button_sound": "button_sound.wav", // Default button sound + "beep_sound": "beep.wav", // Recording start beep + "beep_enabled": true, // Enable beep before recording "greeting_delay_seconds": 0, // Delay before greeting plays (0-10) "volume": 70 // Default volume (0-100) } @@ -597,6 +600,39 @@ Look for your HiFiBerry device and note its index number, then set it in `config 4. Check IP address: `hostname -I` 5. Try localhost: `http://127.0.0.1:8080` +### Audio Sample Rate Mismatch + +If you see errors like `Expression 'paInvalidSampleRate' failed` or warnings about sample rate mismatches: + +1. **Check your config.json sample rate** (default is 48000Hz): + ```json + "audio": { + "sample_rate": 48000 + } + ``` + +2. **Resample your audio files** to match the configured rate: + ```bash + # Using the provided resampling script + python3 resample_audio.py + + # Or manually with ffmpeg + ffmpeg -i input.wav -ar 48000 output.wav + ``` + +3. **Delete default sounds** to regenerate at correct rate: + ```bash + rm rotary_phone_data/sounds/dialtone.wav + rm rotary_phone_data/sounds/beep.wav + # These will be regenerated at startup + ``` + +The `resample_audio.py` utility will: +- Automatically detect the target sample rate from `config.json` +- Create `.backup` files before modifying originals +- Resample all WAV files in the sounds directory +- Preserve stereo/mono and bit depth + ### Configuration Errors If the script won't start: diff --git a/pyproject.toml b/pyproject.toml index 74220f2..4d1627f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,7 @@ dependencies = [ "pyaudio>=0.2.13", "RPi.GPIO>=0.7.1", "waitress>=2.1.0", + "scipy>=1.7.0", ] [project.scripts] diff --git a/resample_audio.py b/resample_audio.py new file mode 100644 index 0000000..4e3c414 --- /dev/null +++ b/resample_audio.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +""" +Audio Resampling Utility for Wedding Phone +Resamples all WAV files in sounds directory to match configured sample rate +""" + +import wave +import numpy as np +import os +import json +from scipy import signal + +def load_config(): + """Load sample rate from config.json""" + config_path = 'config.json' + if not os.path.exists(config_path): + print("config.json not found, using 48000Hz as default") + return 48000 + + with open(config_path, 'r') as f: + config = json.load(f) + return config['audio']['sample_rate'] + +def resample_wav(input_file, output_file, target_rate): + """Resample a WAV file to target sample rate""" + try: + # Open source file + with wave.open(input_file, 'rb') as wf: + n_channels = wf.getnchannels() + sampwidth = wf.getsampwidth() + source_rate = wf.getframerate() + n_frames = wf.getnframes() + + # Read audio data + audio_data = wf.readframes(n_frames) + + # Convert to numpy array + if sampwidth == 1: + dtype = np.uint8 + elif sampwidth == 2: + dtype = np.int16 + elif sampwidth == 4: + dtype = np.int32 + else: + print(f"Unsupported sample width: {sampwidth}") + return False + + audio_array = np.frombuffer(audio_data, dtype=dtype) + + # Handle stereo + if n_channels == 2: + audio_array = audio_array.reshape(-1, 2) + + # Resample + if source_rate != target_rate: + print(f" Resampling from {source_rate}Hz to {target_rate}Hz...") + num_samples = int(len(audio_array) * target_rate / source_rate) + + if n_channels == 1: + resampled = signal.resample(audio_array, num_samples) + else: + # Resample each channel separately + left = signal.resample(audio_array[:, 0], num_samples) + right = signal.resample(audio_array[:, 1], num_samples) + resampled = np.column_stack((left, right)) + + # Convert back to original dtype + resampled = np.clip(resampled, np.iinfo(dtype).min, np.iinfo(dtype).max) + audio_array = resampled.astype(dtype) + + # Write output file + with wave.open(output_file, 'wb') as wf_out: + wf_out.setnchannels(n_channels) + wf_out.setsampwidth(sampwidth) + wf_out.setframerate(target_rate) + wf_out.writeframes(audio_array.tobytes()) + + return True + + except Exception as e: + print(f" Error: {e}") + return False + +def main(): + """Main resampling function""" + print("Wedding Phone Audio Resampler") + print("=" * 50) + + # Load target sample rate + target_rate = load_config() + print(f"\nTarget sample rate: {target_rate}Hz") + + # Check sounds directory + sounds_dir = './rotary_phone_data/sounds' + if not os.path.exists(sounds_dir): + print(f"\nSounds directory not found: {sounds_dir}") + return + + # Get all WAV files + wav_files = [f for f in os.listdir(sounds_dir) if f.endswith('.wav')] + + if not wav_files: + print("\nNo WAV files found in sounds directory") + return + + print(f"\nFound {len(wav_files)} WAV file(s)") + print("-" * 50) + + # Process each file + for filename in wav_files: + input_path = os.path.join(sounds_dir, filename) + print(f"\nProcessing: {filename}") + + # Check current sample rate + try: + with wave.open(input_path, 'rb') as wf: + current_rate = wf.getframerate() + print(f" Current rate: {current_rate}Hz") + + if current_rate == target_rate: + print(f" ✓ Already at {target_rate}Hz, skipping") + continue + except Exception as e: + print(f" Error reading file: {e}") + continue + + # Create backup + backup_path = input_path + '.backup' + if not os.path.exists(backup_path): + os.rename(input_path, backup_path) + print(f" Backup created: {filename}.backup") + else: + input_path = backup_path + print(f" Using existing backup") + + # Resample + output_path = os.path.join(sounds_dir, filename) + if resample_wav(input_path, output_path, target_rate): + print(f" ✓ Successfully resampled to {target_rate}Hz") + else: + print(f" ✗ Failed to resample, restoring backup") + if os.path.exists(backup_path): + os.rename(backup_path, output_path) + + print("\n" + "=" * 50) + print("Resampling complete!") + print("\nBackup files (.backup) have been created.") + print("If everything works, you can delete them.") + +if __name__ == "__main__": + main()