diff --git a/config.example.json b/config.example.json index f86606f..a9a4741 100644 --- a/config.example.json +++ b/config.example.json @@ -35,6 +35,8 @@ "system": { "active_greeting": "dialtone.wav", "extra_button_sound": "button_sound.wav", + "beep_sound": "beep.wav", + "beep_enabled": true, "greeting_delay_seconds": 0, "volume": 70 } diff --git a/rotary_phone_web.py b/rotary_phone_web.py index 6c77b33..66f341f 100644 --- a/rotary_phone_web.py +++ b/rotary_phone_web.py @@ -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.3.0" # Updated: Added download all recordings feature +TEMPLATE_VERSION = "1.4.0" # Updated: Added recording beep sound feature # Flask app app = Flask(__name__) @@ -250,11 +250,18 @@ class RotaryPhone: if not os.path.exists(DIALTONE_FILE): self.generate_default_dialtone() + # Generate default beep if none exists + beep_file = os.path.join(SOUNDS_DIR, "beep.wav") + if not os.path.exists(beep_file): + self.generate_default_beep() + def load_config(self): """Load user runtime configuration from JSON file""" default_config = { "active_greeting": SYS_CONFIG['system']['active_greeting'], "extra_button_sound": SYS_CONFIG['system'].get('extra_button_sound', 'button_sound.wav'), + "beep_sound": SYS_CONFIG['system'].get('beep_sound', 'beep.wav'), + "beep_enabled": SYS_CONFIG['system'].get('beep_enabled', True), "greetings": [], "volume": SYS_CONFIG['system']['volume'], "greeting_delay": SYS_CONFIG['system'].get('greeting_delay_seconds', 0) @@ -269,6 +276,10 @@ class RotaryPhone: config["volume"] = SYS_CONFIG['system']['volume'] if "greeting_delay" not in config: config["greeting_delay"] = SYS_CONFIG['system'].get('greeting_delay_seconds', 0) + if "beep_enabled" not in config: + config["beep_enabled"] = SYS_CONFIG['system'].get('beep_enabled', True) + if "beep_sound" not in config: + config["beep_sound"] = SYS_CONFIG['system'].get('beep_sound', 'beep.wav') return config except: pass @@ -300,6 +311,25 @@ class RotaryPhone: self.config["extra_button_sound"] = filename self.save_config() + def get_beep_sound_path(self): + """Get the full path to the beep sound file""" + sound = self.config.get("beep_sound", "beep.wav") + return os.path.join(SOUNDS_DIR, sound) + + def set_beep_sound(self, filename): + """Set which sound plays as recording beep""" + self.config["beep_sound"] = filename + self.save_config() + + def is_beep_enabled(self): + """Check if beep sound is enabled""" + return self.config.get("beep_enabled", True) + + def set_beep_enabled(self, enabled): + """Enable or disable beep sound""" + self.config["beep_enabled"] = bool(enabled) + self.save_config() + def set_volume(self, volume): """Set playback volume (0-100)""" volume = max(0, min(100, int(volume))) # Clamp between 0-100 @@ -345,7 +375,31 @@ class RotaryPhone: wf.writeframes(tone.tobytes()) wf.close() print(f"Default dial tone saved to {DIALTONE_FILE}") - + + def generate_default_beep(self): + """Generate a simple beep tone (1000Hz) to indicate recording start""" + print("Generating default beep sound...") + beep_file = os.path.join(SOUNDS_DIR, "beep.wav") + duration = 0.5 # Half second beep + sample_rate = 44100 + t = np.linspace(0, duration, int(sample_rate * duration), False) + + # Generate 1000Hz tone + frequency = 1000 + tone = np.sin(2 * np.pi * frequency * t) + + # Convert to 16-bit PCM + tone = (tone * 32767).astype(np.int16) + + # Save as WAV file + wf = wave.open(beep_file, 'wb') + wf.setnchannels(1) + wf.setsampwidth(2) # 16-bit + wf.setframerate(sample_rate) + wf.writeframes(tone.tobytes()) + wf.close() + print(f"Default beep sound saved to {beep_file}") + def play_sound_file(self, filepath, check_hook_status=True): """Play a WAV file with volume control @@ -585,6 +639,12 @@ class RotaryPhone: greeting_file = self.get_active_greeting_path() self.play_sound_file(greeting_file) + # 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() + if os.path.exists(beep_file): + self.play_sound_file(beep_file) + # Start recording if self.phone_status == "off_hook": # Still off hook after sound self.record_audio() @@ -616,6 +676,8 @@ def index(): status = phone.get_status() active_greeting = phone.config.get("active_greeting", "dialtone.wav") extra_button_sound = phone.config.get("extra_button_sound", "button_sound.wav") + beep_sound = phone.config.get("beep_sound", "beep.wav") + beep_enabled = phone.is_beep_enabled() volume = phone.get_volume() greeting_delay = phone.get_greeting_delay() @@ -624,6 +686,8 @@ def index(): greetings=greetings, active_greeting=active_greeting, extra_button_sound=extra_button_sound, + beep_sound=beep_sound, + beep_enabled=beep_enabled, extra_button_enabled=EXTRA_BUTTON_ENABLED, status=status, volume=volume, @@ -763,6 +827,33 @@ def set_extra_button_sound(): phone.set_extra_button_sound(filename) return jsonify({"success": True, "message": f"Extra button sound set to '{filename}'"}) +@app.route('/set_beep_sound', methods=['POST']) +def set_beep_sound(): + """Set which sound plays as recording beep""" + data = request.get_json() + filename = data.get('filename') + + if not filename: + return jsonify({"error": "No filename provided"}), 400 + + filepath = os.path.join(SOUNDS_DIR, filename) + if not os.path.exists(filepath): + return jsonify({"error": "Sound file not found"}), 404 + + phone.set_beep_sound(filename) + return jsonify({"success": True, "message": f"Beep sound set to '{filename}'"}) + +@app.route('/api/beep_enabled', methods=['GET', 'POST']) +def beep_enabled(): + """Get or set beep enabled status""" + if request.method == 'POST': + data = request.get_json() + enabled = data.get('enabled', True) + phone.set_beep_enabled(enabled) + return jsonify({"success": True, "enabled": phone.is_beep_enabled()}) + else: + return jsonify({"enabled": phone.is_beep_enabled()}) + @app.route('/delete_greeting/', methods=['POST']) def delete_greeting(filename): """Delete a greeting file""" @@ -883,7 +974,8 @@ def get_greetings(): "date": datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d %H:%M:%S'), "duration": duration, "is_active": filename == phone.config.get("active_greeting", "dialtone.wav"), - "is_button_sound": filename == phone.config.get("extra_button_sound", "button_sound.wav") + "is_button_sound": filename == phone.config.get("extra_button_sound", "button_sound.wav"), + "is_beep_sound": filename == phone.config.get("beep_sound", "beep.wav") }) return greetings @@ -1432,7 +1524,7 @@ def main(): -
+

Greeting Delay

⏱️ @@ -1445,6 +1537,22 @@ def main(): Delay before greeting plays after pickup (0-10 seconds)

+ + +
+

Recording Beep

+
+ 📣 + +
+

+ Audio cue to signal recording has started +

+
@@ -1511,6 +1619,11 @@ def main(): 🔘 Set Button {% endif %} + {% if not greeting.is_beep_sound %} + + {% endif %} @@ -1523,6 +1636,11 @@ def main(): 🔘 Set Button {% endif %} + {% if not greeting.is_beep_sound %} + + {% endif %} {% endif %}
@@ -1715,6 +1833,25 @@ def main(): .catch(error => console.error('Error updating delay:', error)); } + // Beep enabled checkbox + const beepCheckbox = document.getElementById('beep-enabled'); + beepCheckbox.addEventListener('change', function() { + fetch('/api/beep_enabled', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ enabled: this.checked }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showAlert(data.enabled ? 'Recording beep enabled ✓' : 'Recording beep disabled', 'success'); + } + }) + .catch(error => console.error('Error updating beep setting:', error)); + }); + function handleFileSelect(input) { if (input.files && input.files.length > 0) { selectedFiles = input.files; @@ -1866,6 +2003,26 @@ def main(): .catch(error => showAlert('Error: ' + error, 'error')); } + function setBeepSound(filename) { + fetch('/set_beep_sound', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ filename: filename }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showAlert(`Beep sound set to '${filename}'! ✓`, 'success'); + setTimeout(() => location.reload(), 1500); + } else { + showAlert('Error: ' + data.error, 'error'); + } + }) + .catch(error => showAlert('Error: ' + error, 'error')); + } + function deleteGreeting(filename, index) { if (confirm(`Delete greeting '${filename}'?`)) { fetch('/delete_greeting/' + filename, { method: 'POST' })