Add separate volume controls for greeting, button, and beep sounds

Added individual volume control for each sound type:
- Greeting volume (for welcome messages)
- Button sound volume (for extra button during recording)
- Beep volume (for recording start indicator)

Backend changes:
- Added volume_greeting, volume_button, volume_beep to config
- New getter/setter methods for each volume type
- API endpoints: /api/volume/greeting, /api/volume/button, /api/volume/beep
- Updated play_sound_file() with volume_override parameter
- Greeting, button, and beep playback now use their specific volumes

Frontend changes (template v1.7.0):
- Replaced single volume slider with three separate sliders
- Clean UI with labeled controls for each sound type
- Helper function setupVolumeSlider() for DRY code
- Real-time updates with debouncing (300ms)
- Visual feedback with gradient background

Config changes:
- Added volume_greeting: 70 (default)
- Added volume_button: 70 (default)
- Added volume_beep: 70 (default)
- Backward compatible with existing configs

This allows independent control of each sound's loudness,
useful when greeting needs to be louder than beep, etc.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-27 15:56:12 +07:00
parent e2218a0c9a
commit b219619f24
2 changed files with 175 additions and 48 deletions

View File

@@ -38,6 +38,9 @@
"beep_sound": "beep.wav",
"beep_enabled": true,
"greeting_delay_seconds": 0,
"volume": 70
"volume": 70,
"volume_greeting": 70,
"volume_button": 70,
"volume_beep": 70
}
}

View File

@@ -81,7 +81,7 @@ BACKUP_ON_WRITE = BACKUP_CONFIG.get('backup_on_write', True)
WEB_PORT = SYS_CONFIG['web']['port']
# Template version - increment this when HTML template changes
TEMPLATE_VERSION = "1.6.0" # Updated: Added rename functionality for recordings
TEMPLATE_VERSION = "1.7.0" # Updated: Separate volume controls for greeting, button, and beep
# Flask app
app = Flask(__name__)
@@ -264,6 +264,9 @@ class RotaryPhone:
"beep_enabled": SYS_CONFIG['system'].get('beep_enabled', True),
"greetings": [],
"volume": SYS_CONFIG['system']['volume'],
"volume_greeting": SYS_CONFIG['system'].get('volume_greeting', 70),
"volume_button": SYS_CONFIG['system'].get('volume_button', 70),
"volume_beep": SYS_CONFIG['system'].get('volume_beep', 70),
"greeting_delay": SYS_CONFIG['system'].get('greeting_delay_seconds', 0)
}
@@ -274,6 +277,12 @@ class RotaryPhone:
# Ensure required keys exist
if "volume" not in config:
config["volume"] = SYS_CONFIG['system']['volume']
if "volume_greeting" not in config:
config["volume_greeting"] = SYS_CONFIG['system'].get('volume_greeting', 70)
if "volume_button" not in config:
config["volume_button"] = SYS_CONFIG['system'].get('volume_button', 70)
if "volume_beep" not in config:
config["volume_beep"] = SYS_CONFIG['system'].get('volume_beep', 70)
if "greeting_delay" not in config:
config["greeting_delay"] = SYS_CONFIG['system'].get('greeting_delay_seconds', 0)
if "beep_enabled" not in config:
@@ -341,6 +350,39 @@ class RotaryPhone:
"""Get current volume setting"""
return self.config.get("volume", 70)
def set_volume_greeting(self, volume):
"""Set greeting volume (0-100)"""
volume = max(0, min(100, int(volume)))
self.config["volume_greeting"] = volume
self.save_config()
return volume
def get_volume_greeting(self):
"""Get greeting volume setting"""
return self.config.get("volume_greeting", 70)
def set_volume_button(self, volume):
"""Set button sound volume (0-100)"""
volume = max(0, min(100, int(volume)))
self.config["volume_button"] = volume
self.save_config()
return volume
def get_volume_button(self):
"""Get button sound volume setting"""
return self.config.get("volume_button", 70)
def set_volume_beep(self, volume):
"""Set beep sound volume (0-100)"""
volume = max(0, min(100, int(volume)))
self.config["volume_beep"] = volume
self.save_config()
return volume
def get_volume_beep(self):
"""Get beep sound volume setting"""
return self.config.get("volume_beep", 70)
def set_greeting_delay(self, delay):
"""Set greeting delay in seconds (0-10)"""
delay = max(0, min(10, int(delay))) # Clamp between 0-10
@@ -398,13 +440,14 @@ class RotaryPhone:
wf.close()
print(f"Default beep sound saved to {beep_file} ({RATE}Hz, 16-bit)")
def play_sound_file(self, filepath, check_hook_status=True):
def play_sound_file(self, filepath, check_hook_status=True, volume_override=None):
"""Play a WAV file with volume control
Args:
filepath: Path to WAV file
check_hook_status: If True, only play while off-hook (for greeting).
If False, play completely regardless of hook (for button)
volume_override: Override volume (0-100), or None to use default volume
"""
if not os.path.exists(filepath):
print(f"Sound file not found: {filepath}")
@@ -434,7 +477,7 @@ class RotaryPhone:
)
# Get volume multiplier (0.0 to 1.0)
volume = self.get_volume() / 100.0
volume = (volume_override if volume_override is not None else self.get_volume()) / 100.0
# Play the sound with volume control
data = wf.readframes(CHUNK)
@@ -607,8 +650,9 @@ class RotaryPhone:
frames_per_buffer=CHUNK
)
# Get volume multiplier
volume = self.get_volume() / 100.0
# Get button volume multiplier
button_volume = self.config.get("volume_button", 70)
volume = button_volume / 100.0
print(f"[BUTTON] Volume: {int(volume * 100)}%")
# Play the sound
@@ -680,13 +724,15 @@ class RotaryPhone:
# Play active greeting message
greeting_file = self.get_active_greeting_path()
self.play_sound_file(greeting_file)
greeting_volume = self.config.get("volume_greeting", 70)
self.play_sound_file(greeting_file, volume_override=greeting_volume)
# Play beep sound to indicate recording will start
if self.phone_status == "off_hook" and self.is_beep_enabled():
beep_file = self.get_beep_sound_path()
beep_volume = self.config.get("volume_beep", 70)
if os.path.exists(beep_file):
self.play_sound_file(beep_file)
self.play_sound_file(beep_file, volume_override=beep_volume)
# Start recording
if self.phone_status == "off_hook": # Still off hook after sound
@@ -722,6 +768,9 @@ def index():
beep_sound = phone.config.get("beep_sound", "beep.wav")
beep_enabled = phone.is_beep_enabled()
volume = phone.get_volume()
volume_greeting = phone.get_volume_greeting()
volume_button = phone.get_volume_button()
volume_beep = phone.get_volume_beep()
greeting_delay = phone.get_greeting_delay()
return render_template('index.html',
@@ -734,6 +783,9 @@ def index():
extra_button_enabled=EXTRA_BUTTON_ENABLED,
status=status,
volume=volume,
volume_greeting=volume_greeting,
volume_button=volume_button,
volume_beep=volume_beep,
greeting_delay=greeting_delay)
@app.route('/api/status')
@@ -764,6 +816,45 @@ def api_set_volume():
new_volume = phone.set_volume(volume)
return jsonify({"success": True, "volume": new_volume})
@app.route('/api/volume/greeting', methods=['GET'])
def api_get_volume_greeting():
"""Get greeting volume setting"""
return jsonify({"volume": phone.get_volume_greeting()})
@app.route('/api/volume/greeting', methods=['POST'])
def api_set_volume_greeting():
"""Set greeting volume level"""
data = request.get_json()
volume = data.get('volume', 70)
new_volume = phone.set_volume_greeting(volume)
return jsonify({"success": True, "volume": new_volume})
@app.route('/api/volume/button', methods=['GET'])
def api_get_volume_button():
"""Get button sound volume setting"""
return jsonify({"volume": phone.get_volume_button()})
@app.route('/api/volume/button', methods=['POST'])
def api_set_volume_button():
"""Set button sound volume level"""
data = request.get_json()
volume = data.get('volume', 70)
new_volume = phone.set_volume_button(volume)
return jsonify({"success": True, "volume": new_volume})
@app.route('/api/volume/beep', methods=['GET'])
def api_get_volume_beep():
"""Get beep sound volume setting"""
return jsonify({"volume": phone.get_volume_beep()})
@app.route('/api/volume/beep', methods=['POST'])
def api_set_volume_beep():
"""Set beep sound volume level"""
data = request.get_json()
volume = data.get('volume', 70)
new_volume = phone.set_volume_beep(volume)
return jsonify({"success": True, "volume": new_volume})
@app.route('/api/greeting_delay', methods=['GET'])
def api_get_greeting_delay():
"""Get current greeting delay setting"""
@@ -1606,19 +1697,45 @@ def main():
<div class="card">
<h2>🔊 Volume & Delay Control</h2>
<!-- Volume Control -->
<!-- Volume Controls -->
<div style="padding: 20px; border-bottom: 1px solid #e5e7eb;">
<h3 style="margin: 0 0 15px 0; font-size: 1.1em; color: #333;">Volume</h3>
<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>
<h3 style="margin: 0 0 20px 0; font-size: 1.1em; color: #333;">🔊 Volume Controls</h3>
<!-- Greeting Volume -->
<div style="margin-bottom: 20px;">
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: #555;">📢 Greeting</label>
<div style="display: flex; align-items: center; gap: 15px;">
<span style="font-size: 1.2em;">🔇</span>
<input type="range" id="volume-greeting-slider" min="0" max="100" value="{{ volume_greeting }}"
style="flex: 1; height: 6px; border-radius: 3px; outline: none; background: linear-gradient(to right, #667eea 0%, #667eea {{ volume_greeting }}%, #ddd {{ volume_greeting }}%, #ddd 100%);">
<span style="font-size: 1.2em;">🔊</span>
<span id="volume-greeting-display" style="font-weight: bold; min-width: 45px; text-align: center;">{{ volume_greeting }}%</span>
</div>
</div>
<!-- Button Volume -->
<div style="margin-bottom: 20px;">
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: #555;">🔘 Button Sound</label>
<div style="display: flex; align-items: center; gap: 15px;">
<span style="font-size: 1.2em;">🔇</span>
<input type="range" id="volume-button-slider" min="0" max="100" value="{{ volume_button }}"
style="flex: 1; height: 6px; border-radius: 3px; outline: none; background: linear-gradient(to right, #667eea 0%, #667eea {{ volume_button }}%, #ddd {{ volume_button }}%, #ddd 100%);">
<span style="font-size: 1.2em;">🔊</span>
<span id="volume-button-display" style="font-weight: bold; min-width: 45px; text-align: center;">{{ volume_button }}%</span>
</div>
</div>
<!-- Beep Volume -->
<div>
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: #555;">📣 Recording Beep</label>
<div style="display: flex; align-items: center; gap: 15px;">
<span style="font-size: 1.2em;">🔇</span>
<input type="range" id="volume-beep-slider" min="0" max="100" value="{{ volume_beep }}"
style="flex: 1; height: 6px; border-radius: 3px; outline: none; background: linear-gradient(to right, #667eea 0%, #667eea {{ volume_beep }}%, #ddd {{ volume_beep }}%, #ddd 100%);">
<span style="font-size: 1.2em;">🔊</span>
<span id="volume-beep-display" style="font-weight: bold; min-width: 45px; text-align: center;">{{ volume_beep }}%</span>
</div>
</div>
<p style="margin-top: 10px; color: #6b7280; font-size: 0.85em; text-align: center;">
Playback volume for greeting messages
</p>
</div>
<!-- Greeting Delay Control -->
@@ -1875,43 +1992,50 @@ def main():
<script>
let selectedFiles = null;
let volumeUpdateTimer = null;
let volumeGreetingTimer = null;
let volumeButtonTimer = null;
let volumeBeepTimer = null;
let delayUpdateTimer = null;
// Volume control
const volumeSlider = document.getElementById('volume-slider');
const volumeDisplay = document.getElementById('volume-display');
// Volume control helper function
function setupVolumeSlider(sliderId, displayId, apiEndpoint) {
const slider = document.getElementById(sliderId);
const display = document.getElementById(displayId);
let timer = null;
volumeSlider.addEventListener('input', function() {
const value = this.value;
volumeDisplay.textContent = value + '%';
if (!slider) return;
// Update slider background gradient
this.style.background = `linear-gradient(to right, #667eea 0%, #667eea ${value}%, #ddd ${value}%, #ddd 100%)`;
slider.addEventListener('input', function() {
const value = this.value;
display.textContent = value + '%';
// Debounce API call
clearTimeout(volumeUpdateTimer);
volumeUpdateTimer = setTimeout(() => {
updateVolume(value);
}, 300);
});
// Update slider background gradient
this.style.background = `linear-gradient(to right, #667eea 0%, #667eea ${value}%, #ddd ${value}%, #ddd 100%)`;
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));
// Debounce API call
clearTimeout(timer);
timer = setTimeout(() => {
fetch(apiEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ volume: parseInt(value) })
})
.then(response => response.json())
.then(data => {
if (data.success) {
console.log(`Volume updated to ${data.volume}%`);
}
})
.catch(error => console.error('Error updating volume:', error));
}, 300);
});
}
// Setup all volume sliders
setupVolumeSlider('volume-greeting-slider', 'volume-greeting-display', '/api/volume/greeting');
setupVolumeSlider('volume-button-slider', 'volume-button-display', '/api/volume/button');
setupVolumeSlider('volume-beep-slider', 'volume-beep-display', '/api/volume/beep');
// Greeting Delay control
const delaySlider = document.getElementById('delay-slider');
const delayDisplay = document.getElementById('delay-display');