feat(call): update tone generator functionality with volume control and enable/disable settings; update frontend components to reflect new configurations

This commit is contained in:
2026-01-04 17:48:07 -06:00
parent 0a65619efb
commit 2f65bde2d3
9 changed files with 238 additions and 32 deletions

View File

@@ -186,6 +186,16 @@ class ConfigManager:
"call_recording_enabled",
False,
)
self.telephone_tone_generator_enabled = self.BoolConfig(
self,
"telephone_tone_generator_enabled",
True,
)
self.telephone_tone_generator_volume = self.IntConfig(
self,
"telephone_tone_generator_volume",
50,
)
# map config
self.map_offline_enabled = self.BoolConfig(self, "map_offline_enabled", False)

View File

@@ -272,6 +272,8 @@ class TelephoneManager:
# Wait for identity to appear
start_wait = time.time()
while time.time() - start_wait < timeout_seconds:
if not self.initiation_status: # Externally cancelled (hangup)
return None
await asyncio.sleep(0.5)
destination_identity = resolve_identity(destination_hash_hex)
if destination_identity:
@@ -289,6 +291,8 @@ class TelephoneManager:
# Wait up to 10s for path discovery
path_wait_start = time.time()
while time.time() - path_wait_start < min(timeout_seconds, 10):
if not self.initiation_status: # Externally cancelled
return None
if RNS.Transport.has_path(destination_hash):
break
await asyncio.sleep(0.5)
@@ -307,6 +311,8 @@ class TelephoneManager:
# LXST telephone.call usually returns on establishment or timeout.
# We wait for it, but if status becomes established or ended, we can stop waiting.
while not call_task.done():
if not self.initiation_status: # Externally cancelled
break
if self.telephone.call_status in [
6,
0,
@@ -321,9 +327,14 @@ class TelephoneManager:
# If the task finished but we're still ringing or connecting,
# wait a bit more for establishment or definitive failure
if self.telephone.call_status in [4, 5]: # Ringing, Connecting
if self.initiation_status and self.telephone.call_status in [
4,
5,
]: # Ringing, Connecting
wait_until = time.time() + timeout_seconds
while time.time() < wait_until:
if not self.initiation_status: # Externally cancelled
break
if self.telephone.call_status in [
6,
0,

View File

@@ -745,7 +745,10 @@ export default {
this.initiationTargetName = json.target_name;
if (this.initiationStatus === "Ringing...") {
this.toneGenerator.playRingback();
if (this.config?.telephone_tone_generator_enabled) {
this.toneGenerator.setVolume(this.config.telephone_tone_generator_volume);
this.toneGenerator.playRingback();
}
} else if (this.initiationStatus === null) {
this.toneGenerator.stop();
}
@@ -768,7 +771,10 @@ export default {
case "telephone_call_ended": {
this.stopRingtone();
this.ringtonePlayer = null;
this.toneGenerator.playBusyTone();
if (this.config?.telephone_tone_generator_enabled) {
this.toneGenerator.setVolume(this.config.telephone_tone_generator_volume);
this.toneGenerator.playBusyTone();
}
this.updateTelephoneStatus();
break;
}
@@ -850,6 +856,7 @@ export default {
const response = await window.axios.get(`/api/v1/config`);
this.config = response.data.config;
GlobalState.config = response.data.config;
this.displayName = response.data.config.display_name;
} catch (e) {
// do nothing if failed to load config
console.log(e);
@@ -1067,7 +1074,10 @@ export default {
const justEnded = oldCall != null && this.activeCall == null;
if (justEnded) {
this.lastCall = oldCall;
this.toneGenerator.playBusyTone();
if (this.config?.telephone_tone_generator_enabled) {
this.toneGenerator.setVolume(this.config.telephone_tone_generator_volume);
this.toneGenerator.playBusyTone();
}
// Trigger history refresh
GlobalEmitter.emit("telephone-history-updated");
@@ -1086,7 +1096,10 @@ export default {
// Handle outgoing ringback tone
if (this.initiationStatus === "Ringing...") {
this.toneGenerator.playRingback();
if (this.config?.telephone_tone_generator_enabled) {
this.toneGenerator.setVolume(this.config.telephone_tone_generator_volume);
this.toneGenerator.playRingback();
}
} else if (!this.initiationStatus && !this.activeCall && !this.isCallEnded) {
// Only stop if we're not ringing, in a call, or just finished a call (busy tone playing)
this.toneGenerator.stop();

View File

@@ -661,10 +661,26 @@
</div>
<div
class="text-[10px] font-mono text-gray-400 dark:text-zinc-600 truncate mt-0.5 cursor-pointer hover:text-blue-500 transition-colors"
:title="entry.remote_telephony_hash || entry.remote_destination_hash || entry.remote_identity_hash"
@click.stop="copyHash(entry.remote_telephony_hash || entry.remote_destination_hash || entry.remote_identity_hash)"
:title="
entry.remote_telephony_hash ||
entry.remote_destination_hash ||
entry.remote_identity_hash
"
@click.stop="
copyHash(
entry.remote_telephony_hash ||
entry.remote_destination_hash ||
entry.remote_identity_hash
)
"
>
{{ formatDestinationHash(entry.remote_telephony_hash || entry.remote_destination_hash || entry.remote_identity_hash) }}
{{
formatDestinationHash(
entry.remote_telephony_hash ||
entry.remote_destination_hash ||
entry.remote_identity_hash
)
}}
</div>
</div>
@@ -692,8 +708,12 @@
type="button"
class="flex items-center gap-1.5 px-3 py-1 bg-blue-600 text-white rounded-lg text-[10px] font-bold hover:bg-blue-500 transition-all shadow-md shadow-blue-500/10 shrink-0"
@click="
destinationHash = entry.remote_identity_hash;
call(destinationHash);
destinationHash =
entry.remote_telephony_hash ||
entry.remote_destination_hash ||
entry.remote_identity_hash;
activeTab = 'phone';
$nextTick(() => call(destinationHash));
"
>
<MaterialDesignIcon icon-name="phone" class="size-3" />
@@ -819,7 +839,7 @@
@click="
destinationHash = announce.destination_hash;
activeTab = 'phone';
call(destinationHash);
$nextTick(() => call(destinationHash));
"
>
Call
@@ -1249,9 +1269,12 @@
type="button"
class="text-[10px] flex items-center gap-1 text-gray-500 hover:text-blue-500 font-bold uppercase tracking-wider transition-colors"
@click="
destinationHash = voicemail.remote_telephony_hash || voicemail.remote_destination_hash || voicemail.remote_identity_hash;
destinationHash =
voicemail.remote_telephony_hash ||
voicemail.remote_destination_hash ||
voicemail.remote_identity_hash;
activeTab = 'phone';
call(destinationHash);
$nextTick(() => call(destinationHash));
"
>
<MaterialDesignIcon icon-name="phone" class="size-3" />
@@ -1383,9 +1406,12 @@
type="button"
class="text-[10px] bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400 px-3 py-1 rounded-full font-bold uppercase tracking-wider hover:bg-blue-200 dark:hover:bg-blue-900/50 transition-colors"
@click="
destinationHash = contact.remote_identity_hash;
destinationHash =
contact.remote_telephony_hash ||
contact.remote_destination_hash ||
contact.remote_identity_hash;
activeTab = 'phone';
call(destinationHash);
$nextTick(() => call(destinationHash));
"
>
Call
@@ -1478,6 +1504,73 @@
</div>
</div>
<!-- Tone Generator Settings -->
<div
class="flex flex-col md:flex-row md:items-center justify-between gap-6 pt-4 border-t border-gray-100 dark:border-zinc-800/50"
>
<div class="flex-1">
<div class="flex items-center justify-between mb-1">
<div class="text-sm font-semibold text-gray-900 dark:text-white">
Tone Generator
</div>
<button
class="relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none"
:class="
config.telephone_tone_generator_enabled
? 'bg-blue-600'
: 'bg-gray-200 dark:bg-zinc-700'
"
@click="
config.telephone_tone_generator_enabled =
!config.telephone_tone_generator_enabled;
updateConfig({
telephone_tone_generator_enabled:
config.telephone_tone_generator_enabled,
});
"
>
<span
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
:class="
config.telephone_tone_generator_enabled
? 'translate-x-5'
: 'translate-x-0'
"
></span>
</button>
</div>
<div class="text-xs text-gray-500 dark:text-zinc-400">
Play audio feedback during call dialing and disconnection.
</div>
</div>
<div v-if="config.telephone_tone_generator_enabled" class="flex-1 md:max-w-xs">
<div class="flex items-center justify-between mb-2">
<label
class="text-xs font-bold text-gray-500 dark:text-zinc-400 uppercase tracking-wider"
>
Tone Volume
</label>
<span class="text-xs font-mono text-gray-400"
>{{ config.telephone_tone_generator_volume }}%</span
>
</div>
<input
v-model.number="config.telephone_tone_generator_volume"
type="range"
min="0"
max="100"
class="w-full h-1.5 bg-gray-200 dark:bg-zinc-700 rounded-lg appearance-none cursor-pointer accent-blue-600"
@change="
updateConfig({
telephone_tone_generator_volume:
config.telephone_tone_generator_volume,
})
"
/>
</div>
</div>
<!-- Preferred Ringtone for Non-Contacts -->
<div
class="p-4 rounded-xl bg-blue-50/50 dark:bg-blue-900/10 border border-blue-100/50 dark:border-blue-900/30"
@@ -2188,8 +2281,10 @@ export default {
},
async updateConfig(config) {
try {
await window.axios.patch("/api/v1/config", config);
await this.getConfig();
const response = await window.axios.patch("/api/v1/config", config);
if (response.data?.config) {
this.config = response.data.config;
}
ToastUtils.success("Settings saved");
} catch {
ToastUtils.error("Failed to save settings");

View File

@@ -902,6 +902,8 @@ export default {
banished_text: "BANISHED",
banished_color: "#dc2626",
blackhole_integration_enabled: true,
telephone_tone_generator_enabled: true,
telephone_tone_generator_volume: 50,
},
saveTimeouts: {},
shortcuts: [],

View File

@@ -5,6 +5,15 @@ export default class ToneGenerator {
this.gainNode = null;
this.timeoutId = null;
this.currentTone = null; // 'ringback', 'busy', or null
this.volume = 0.1; // Default volume (0.0 to 1.0 real gain)
}
setVolume(volumePercent) {
// volumePercent is 0-100
this.volume = (volumePercent / 100) * 0.2; // Cap at 0.2 real gain for safety
if (this.gainNode && this.audioCtx) {
this.gainNode.gain.setTargetAtTime(this.volume, this.audioCtx.currentTime, 0.1);
}
}
_initAudioContext() {
@@ -26,7 +35,7 @@ export default class ToneGenerator {
osc1.frequency.value = 440;
osc2.frequency.value = 480;
gain.gain.value = 0.1;
gain.gain.value = this.volume;
osc1.connect(gain);
osc2.connect(gain);
@@ -70,7 +79,7 @@ export default class ToneGenerator {
const gain = this.audioCtx.createGain();
osc.frequency.value = 480;
gain.gain.value = 0.1;
gain.gain.value = this.volume;
osc.connect(gain);
gain.connect(this.audioCtx.destination);