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:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user