Add UV package management and volume control feature
Features: - Add pyproject.toml for UV package management - Volume control with real-time slider (0-100%) - Backend volume adjustment with numpy audio scaling - Volume setting persists in config.json - Debounced API calls for smooth slider interaction - Enhanced audio playback with volume multiplier - Update README with UV installation instructions - Add volume control documentation API Changes: - GET /api/volume - Get current volume setting - POST /api/volume - Set volume level 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
49
README.md
49
README.md
@@ -9,6 +9,7 @@ A Raspberry Pi-based rotary phone system for weddings and events. Guests can pic
|
||||
- **Voice Recording**: Automatically records guest messages after the greeting
|
||||
- **Web Interface**: Beautiful, responsive web UI for managing the system
|
||||
- **Audio Playback**: Play recordings and greetings directly in the browser
|
||||
- **Volume Control**: Adjust playback volume with real-time slider (0-100%)
|
||||
- **Multiple Message Support**: Upload and manage multiple greeting messages
|
||||
- **Active Message Selector**: Choose which greeting plays when the phone is picked up
|
||||
- **HiFiBerry Support**: Optimized for HiFiBerry DAC+ADC Pro audio quality
|
||||
@@ -43,7 +44,30 @@ git clone https://git.b4l.co.th/grabowski/wedding-phone.git
|
||||
cd wedding-phone
|
||||
```
|
||||
|
||||
### 2. Install Dependencies
|
||||
### 2. Install UV (Recommended)
|
||||
|
||||
UV is a fast Python package installer and resolver. Install it:
|
||||
|
||||
```bash
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
# Or on Raspberry Pi with pip:
|
||||
pip3 install uv
|
||||
```
|
||||
|
||||
### 3. Install Dependencies
|
||||
|
||||
#### Option A: Using UV (Recommended)
|
||||
|
||||
```bash
|
||||
# Install system dependencies
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y python3-pyaudio portaudio19-dev
|
||||
|
||||
# Install Python dependencies with UV
|
||||
uv pip install -e .
|
||||
```
|
||||
|
||||
#### Option B: Using pip
|
||||
|
||||
```bash
|
||||
sudo apt-get update
|
||||
@@ -51,7 +75,7 @@ sudo apt-get install -y python3-pip python3-pyaudio portaudio19-dev
|
||||
pip3 install flask numpy RPi.GPIO
|
||||
```
|
||||
|
||||
### 3. Configure HiFiBerry
|
||||
### 4. Configure HiFiBerry
|
||||
|
||||
Run the automatic configuration script:
|
||||
|
||||
@@ -62,7 +86,7 @@ chmod +x configure_hifiberry.sh
|
||||
|
||||
Or follow the manual instructions in `AUDIO_FIX.md`.
|
||||
|
||||
### 4. Test Your Audio
|
||||
### 5. Test Your Audio
|
||||
|
||||
```bash
|
||||
python3 test_complete.py
|
||||
@@ -73,7 +97,7 @@ This will test:
|
||||
- Dial tone generation
|
||||
- Microphone recording
|
||||
|
||||
### 5. Configure GPIO Pin
|
||||
### 6. Configure GPIO Pin
|
||||
|
||||
Edit `rotary_phone_web.py` and set your hookswitch GPIO pin:
|
||||
|
||||
@@ -82,7 +106,7 @@ HOOK_PIN = 17 # Change to your GPIO pin number
|
||||
HOOK_PRESSED = GPIO.LOW # Or GPIO.HIGH depending on your switch
|
||||
```
|
||||
|
||||
### 6. Run the System
|
||||
### 7. Run the System
|
||||
|
||||
```bash
|
||||
python3 rotary_phone_web.py
|
||||
@@ -96,21 +120,27 @@ The web interface will be available at:
|
||||
|
||||
### Web Interface
|
||||
|
||||
The web interface provides three main sections:
|
||||
The web interface provides four main sections:
|
||||
|
||||
#### 1. Phone Status
|
||||
- Shows current phone state (on-hook/off-hook/recording)
|
||||
- Displays active recording filename
|
||||
- Auto-refreshes every 5 seconds
|
||||
|
||||
#### 2. Greeting Messages
|
||||
#### 2. Volume Control
|
||||
- **Adjust Volume**: Drag slider to set playback volume (0-100%)
|
||||
- Real-time visual feedback with percentage display
|
||||
- Changes apply immediately to greeting playback
|
||||
- Volume setting persists across restarts
|
||||
|
||||
#### 3. Greeting Messages
|
||||
- **Upload**: Click "Choose WAV File(s)" to upload one or multiple greeting messages
|
||||
- **Play**: Click "▶️ Play" to preview any greeting in your browser
|
||||
- **Set Active**: Click "⭐ Set Active" to select which greeting plays when the phone is picked up
|
||||
- **Delete**: Remove unwanted greetings (cannot delete the active one)
|
||||
- **Default Tone**: Generate a classic telephone dial tone
|
||||
|
||||
#### 3. Recordings
|
||||
#### 4. Recordings
|
||||
- **Play**: Listen to recordings directly in the browser
|
||||
- **Download**: Save recordings to your computer
|
||||
- **Delete**: Remove unwanted recordings
|
||||
@@ -131,6 +161,7 @@ wedding-phone/
|
||||
├── rotary_phone_web.py # Main application
|
||||
├── test_complete.py # Audio testing script
|
||||
├── configure_hifiberry.sh # HiFiBerry setup script
|
||||
├── pyproject.toml # UV/pip package configuration
|
||||
├── AUDIO_FIX.md # Audio configuration guide
|
||||
├── README.md # This file
|
||||
├── .gitignore # Git ignore rules
|
||||
@@ -279,6 +310,8 @@ The system provides REST API endpoints:
|
||||
- `GET /api/status` - Phone status JSON
|
||||
- `GET /api/recordings` - List all recordings
|
||||
- `GET /api/greetings` - List all greeting messages
|
||||
- `GET /api/volume` - Get current volume setting
|
||||
- `POST /api/volume` - Set volume level (0-100)
|
||||
- `POST /upload_greeting` - Upload new greeting
|
||||
- `POST /set_active_greeting` - Set active greeting
|
||||
- `POST /delete_greeting/<filename>` - Delete greeting
|
||||
|
||||
24
pyproject.toml
Normal file
24
pyproject.toml
Normal file
@@ -0,0 +1,24 @@
|
||||
[project]
|
||||
name = "wedding-phone"
|
||||
version = "1.0.0"
|
||||
description = "Vintage rotary phone audio system for weddings and events"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.7"
|
||||
dependencies = [
|
||||
"flask>=2.3.0",
|
||||
"numpy>=1.21.0",
|
||||
"pyaudio>=0.2.13",
|
||||
"RPi.GPIO>=0.7.1",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"pytest>=7.0.0",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.uv]
|
||||
dev-dependencies = []
|
||||
@@ -67,13 +67,18 @@ class RotaryPhone:
|
||||
"""Load configuration from JSON file"""
|
||||
default_config = {
|
||||
"active_greeting": "dialtone.wav",
|
||||
"greetings": []
|
||||
"greetings": [],
|
||||
"volume": 70 # Default volume percentage (0-100)
|
||||
}
|
||||
|
||||
if os.path.exists(CONFIG_FILE):
|
||||
try:
|
||||
with open(CONFIG_FILE, 'r') as f:
|
||||
return json.load(f)
|
||||
config = json.load(f)
|
||||
# Ensure volume key exists
|
||||
if "volume" not in config:
|
||||
config["volume"] = 70
|
||||
return config
|
||||
except:
|
||||
pass
|
||||
|
||||
@@ -93,6 +98,17 @@ class RotaryPhone:
|
||||
"""Set which greeting message to play"""
|
||||
self.config["active_greeting"] = filename
|
||||
self.save_config()
|
||||
|
||||
def set_volume(self, volume):
|
||||
"""Set playback volume (0-100)"""
|
||||
volume = max(0, min(100, int(volume))) # Clamp between 0-100
|
||||
self.config["volume"] = volume
|
||||
self.save_config()
|
||||
return volume
|
||||
|
||||
def get_volume(self):
|
||||
"""Get current volume setting"""
|
||||
return self.config.get("volume", 70)
|
||||
|
||||
def generate_default_dialtone(self):
|
||||
"""Generate a classic dial tone (350Hz + 440Hz) and save as default"""
|
||||
@@ -119,16 +135,16 @@ class RotaryPhone:
|
||||
print(f"Default dial tone saved to {DIALTONE_FILE}")
|
||||
|
||||
def play_sound_file(self, filepath):
|
||||
"""Play a WAV file"""
|
||||
"""Play a WAV file with volume control"""
|
||||
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()),
|
||||
@@ -138,19 +154,28 @@ class RotaryPhone:
|
||||
output_device_index=1, # HiFiBerry device index
|
||||
frames_per_buffer=CHUNK
|
||||
)
|
||||
|
||||
# Play the sound
|
||||
|
||||
# Get volume multiplier (0.0 to 1.0)
|
||||
volume = self.get_volume() / 100.0
|
||||
|
||||
# Play the sound with volume control
|
||||
data = wf.readframes(CHUNK)
|
||||
while data and self.phone_status == "off_hook":
|
||||
# Apply volume by converting to numpy array and scaling
|
||||
if volume < 1.0:
|
||||
audio_data = np.frombuffer(data, dtype=np.int16)
|
||||
audio_data = (audio_data * volume).astype(np.int16)
|
||||
data = audio_data.tobytes()
|
||||
|
||||
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
|
||||
@@ -269,12 +294,14 @@ def index():
|
||||
greetings = get_greetings()
|
||||
status = phone.get_status()
|
||||
active_greeting = phone.config.get("active_greeting", "dialtone.wav")
|
||||
volume = phone.get_volume()
|
||||
|
||||
return render_template('index.html',
|
||||
recordings=recordings,
|
||||
greetings=greetings,
|
||||
active_greeting=active_greeting,
|
||||
status=status)
|
||||
status=status,
|
||||
volume=volume)
|
||||
|
||||
@app.route('/api/status')
|
||||
def api_status():
|
||||
@@ -291,6 +318,19 @@ def api_greetings():
|
||||
"""API endpoint for greetings list"""
|
||||
return jsonify(get_greetings())
|
||||
|
||||
@app.route('/api/volume', methods=['GET'])
|
||||
def api_get_volume():
|
||||
"""Get current volume setting"""
|
||||
return jsonify({"volume": phone.get_volume()})
|
||||
|
||||
@app.route('/api/volume', methods=['POST'])
|
||||
def api_set_volume():
|
||||
"""Set volume level"""
|
||||
data = request.get_json()
|
||||
volume = data.get('volume', 70)
|
||||
new_volume = phone.set_volume(volume)
|
||||
return jsonify({"success": True, "volume": new_volume})
|
||||
|
||||
@app.route('/upload_greeting', methods=['POST'])
|
||||
def upload_greeting():
|
||||
"""Upload a new greeting message"""
|
||||
@@ -835,6 +875,46 @@ if __name__ == "__main__":
|
||||
margin-bottom: 10px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* Volume slider */
|
||||
input[type="range"] {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type="range"]::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: #667eea;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
input[type="range"]::-webkit-slider-thumb:hover {
|
||||
background: #5568d3;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
input[type="range"]::-moz-range-thumb {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: #667eea;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
input[type="range"]::-moz-range-thumb:hover {
|
||||
background: #5568d3;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -870,7 +950,24 @@ if __name__ == "__main__":
|
||||
</div>
|
||||
<button class="btn btn-secondary" onclick="refreshStatus()">🔄 Refresh</button>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Volume Control Card -->
|
||||
<div class="card">
|
||||
<h2>🔊 Volume Control</h2>
|
||||
<div style="padding: 20px;">
|
||||
<div style="display: flex; align-items: center; gap: 20px;">
|
||||
<span style="font-size: 1.5em;">🔇</span>
|
||||
<input type="range" id="volume-slider" min="0" max="100" value="{{ volume }}"
|
||||
style="flex: 1; height: 8px; border-radius: 5px; outline: none; background: linear-gradient(to right, #667eea 0%, #667eea {{ volume }}%, #ddd {{ volume }}%, #ddd 100%);">
|
||||
<span style="font-size: 1.5em;">🔊</span>
|
||||
<span id="volume-display" style="font-weight: bold; min-width: 50px; text-align: center; font-size: 1.2em;">{{ volume }}%</span>
|
||||
</div>
|
||||
<p style="margin-top: 15px; color: #6b7280; font-size: 0.9em; text-align: center;">
|
||||
Adjust the playback volume for greeting messages
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Greeting Messages Card -->
|
||||
<div class="card">
|
||||
<h2>🎵 Greeting Messages</h2>
|
||||
@@ -1017,7 +1114,43 @@ if __name__ == "__main__":
|
||||
|
||||
<script>
|
||||
let selectedFiles = null;
|
||||
|
||||
let volumeUpdateTimer = null;
|
||||
|
||||
// Volume control
|
||||
const volumeSlider = document.getElementById('volume-slider');
|
||||
const volumeDisplay = document.getElementById('volume-display');
|
||||
|
||||
volumeSlider.addEventListener('input', function() {
|
||||
const value = this.value;
|
||||
volumeDisplay.textContent = value + '%';
|
||||
|
||||
// Update slider background gradient
|
||||
this.style.background = `linear-gradient(to right, #667eea 0%, #667eea ${value}%, #ddd ${value}%, #ddd 100%)`;
|
||||
|
||||
// Debounce API call
|
||||
clearTimeout(volumeUpdateTimer);
|
||||
volumeUpdateTimer = setTimeout(() => {
|
||||
updateVolume(value);
|
||||
}, 300);
|
||||
});
|
||||
|
||||
function updateVolume(volume) {
|
||||
fetch('/api/volume', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ volume: parseInt(volume) })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
console.log('Volume updated to ' + data.volume + '%');
|
||||
}
|
||||
})
|
||||
.catch(error => console.error('Error updating volume:', error));
|
||||
}
|
||||
|
||||
function handleFileSelect(input) {
|
||||
if (input.files && input.files.length > 0) {
|
||||
selectedFiles = input.files;
|
||||
|
||||
Reference in New Issue
Block a user