Add audio resampling utility and scipy dependency

- Created resample_audio.py utility script
  - Automatically reads target sample rate from config.json
  - Resamples all WAV files in sounds directory
  - Creates .backup files before modifying originals
  - Handles both mono and stereo audio
  - Uses scipy.signal.resample for high-quality resampling

- Added scipy>=1.7.0 dependency to pyproject.toml
- Updated Makefile sync command to include scipy
- Updated README.md with sample rate troubleshooting section
- Updated config example in README to show 48kHz default
- Added beep_sound configuration to README system section

This resolves sample rate mismatch errors when audio files
don't match the configured rate in config.json.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-27 13:02:39 +07:00
parent 52b8348a03
commit 30ac7e89e9
4 changed files with 190 additions and 2 deletions

View File

@@ -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!"

View File

@@ -97,6 +97,7 @@ Web interface available at: `http://<raspberry-pi-ip>: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:

View File

@@ -10,6 +10,7 @@ dependencies = [
"pyaudio>=0.2.13",
"RPi.GPIO>=0.7.1",
"waitress>=2.1.0",
"scipy>=1.7.0",
]
[project.scripts]

151
resample_audio.py Normal file
View File

@@ -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()