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:
2
Makefile
2
Makefile
@@ -13,7 +13,7 @@ test:
|
|||||||
|
|
||||||
sync:
|
sync:
|
||||||
@echo "Installing/syncing dependencies..."
|
@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
|
install: sync
|
||||||
@echo "Dependencies installed!"
|
@echo "Dependencies installed!"
|
||||||
|
|||||||
38
README.md
38
README.md
@@ -97,6 +97,7 @@ Web interface available at: `http://<raspberry-pi-ip>:8080`
|
|||||||
- pyaudio>=0.2.13
|
- pyaudio>=0.2.13
|
||||||
- RPi.GPIO>=0.7.1
|
- RPi.GPIO>=0.7.1
|
||||||
- waitress>=2.1.0 (production WSGI server)
|
- waitress>=2.1.0 (production WSGI server)
|
||||||
|
- scipy>=1.7.0 (audio resampling utility)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -506,7 +507,7 @@ The `config.json` file contains all system settings:
|
|||||||
"chunk_size": 1024, // Audio buffer size
|
"chunk_size": 1024, // Audio buffer size
|
||||||
"format": "paInt16", // Audio format (16-bit)
|
"format": "paInt16", // Audio format (16-bit)
|
||||||
"channels": 1, // Mono audio
|
"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)
|
"max_record_seconds": 300 // Max recording time (5 minutes)
|
||||||
},
|
},
|
||||||
"paths": {
|
"paths": {
|
||||||
@@ -527,6 +528,8 @@ The `config.json` file contains all system settings:
|
|||||||
"system": {
|
"system": {
|
||||||
"active_greeting": "dialtone.wav", // Default greeting
|
"active_greeting": "dialtone.wav", // Default greeting
|
||||||
"extra_button_sound": "button_sound.wav", // Default button sound
|
"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)
|
"greeting_delay_seconds": 0, // Delay before greeting plays (0-10)
|
||||||
"volume": 70 // Default volume (0-100)
|
"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`
|
4. Check IP address: `hostname -I`
|
||||||
5. Try localhost: `http://127.0.0.1:8080`
|
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
|
### Configuration Errors
|
||||||
|
|
||||||
If the script won't start:
|
If the script won't start:
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ dependencies = [
|
|||||||
"pyaudio>=0.2.13",
|
"pyaudio>=0.2.13",
|
||||||
"RPi.GPIO>=0.7.1",
|
"RPi.GPIO>=0.7.1",
|
||||||
"waitress>=2.1.0",
|
"waitress>=2.1.0",
|
||||||
|
"scipy>=1.7.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|||||||
151
resample_audio.py
Normal file
151
resample_audio.py
Normal 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()
|
||||||
Reference in New Issue
Block a user