diff --git a/overlay/rootfs/etc/init.d/msm8909-modem-wwan-port b/overlay/rootfs/etc/init.d/msm8909-modem-wwan-port index ccb14f6..a9591d5 100755 --- a/overlay/rootfs/etc/init.d/msm8909-modem-wwan-port +++ b/overlay/rootfs/etc/init.d/msm8909-modem-wwan-port @@ -30,5 +30,12 @@ start() { if [ -n "$QMICLI_MODEM" ]; then $QMICLI_MODEM --dpm-open-port=hw-data-ep-type=bam-dmux,hw-data-ep-iface-number=1,hw-data-rx-id=1,hw-data-tx-id=1 >/dev/null 2>&1 || true fi + + for f in /etc/NetworkManager/system-connections/*.nmconnection; do + [ -f "$f" ] || continue + if grep -q "^type=gsm" "$f" 2>/dev/null; then + sed -i '/^\[ipv6\]/,/^\[/ s/^method=auto$/method=ignore/' "$f" 2>/dev/null || true + fi + done eend 0 } diff --git a/overlay/rootfs/home/user/.config/sxmo/ringsettings b/overlay/rootfs/home/user/.config/sxmo/ringsettings new file mode 100644 index 0000000..ce70c36 --- /dev/null +++ b/overlay/rootfs/home/user/.config/sxmo/ringsettings @@ -0,0 +1,4 @@ +SXMO_RINGTIME=20 +SXMO_RINGNUMBER=10 +SXMO_VIBRATE_STRENGTH=2 +SXMO_RINGVOL=100 diff --git a/overlay/rootfs/home/user/.config/sxmo/ringtone b/overlay/rootfs/home/user/.config/sxmo/ringtone new file mode 100644 index 0000000..8e58a9b --- /dev/null +++ b/overlay/rootfs/home/user/.config/sxmo/ringtone @@ -0,0 +1 @@ +/usr/share/sxmo/ringtones/Banana_Chime.wav diff --git a/overlay/rootfs/usr/bin/sxmo_modem.sh b/overlay/rootfs/usr/bin/sxmo_modem.sh index d641fb8..9e506e7 100755 --- a/overlay/rootfs/usr/bin/sxmo_modem.sh +++ b/overlay/rootfs/usr/bin/sxmo_modem.sh @@ -2,6 +2,8 @@ # SPDX-License-Identifier: AGPL-3.0-only # Copyright 2022 Sxmo Contributors +export PATH="${XDG_CONFIG_HOME:-${HOME:-/home/user}/.config}/sxmo/hooks:/usr/share/sxmo/default_hooks:$PATH" + # include common definitions # shellcheck source=configs/default_hooks/sxmo_hook_icons.sh . sxmo_hook_icons.sh @@ -107,9 +109,10 @@ checkforfinishedcalls() { # If it was the last call if ! sxmo_modemcall.sh list_active_calls | grep -q .; then # Cleanup + sxmo_jobs.sh stop ringing rm -f "$XDG_RUNTIME_DIR/sxmo_calls/"* 2>/dev/null || true - if [ "$STATE" != muted ]; then - sxmo_vibrate 1000 "${SXMO_VIBRATE_STRENGTH:-1}" & + if [ "$STATE" != muted ] && [ ! -f "$XDG_CONFIG_HOME/sxmo/.novibrate" ]; then + sxmo_vibrate 600 & fi sxmo_jobs.sh stop incall_menu sxmo_jobs.sh stop proximity_lock diff --git a/overlay/rootfs/usr/bin/sxmo_modemaudio.sh b/overlay/rootfs/usr/bin/sxmo_modemaudio.sh index 9f75421..2a0f59b 100755 --- a/overlay/rootfs/usr/bin/sxmo_modemaudio.sh +++ b/overlay/rootfs/usr/bin/sxmo_modemaudio.sh @@ -6,19 +6,26 @@ export PATH="${XDG_CONFIG_HOME:-$HOME/.config}/sxmo/hooks:/usr/share/sxmo/defaul . sxmo_common.sh enable_call_audio_mode() { + pactl set-card-profile alsa_card.comprC0 off >/dev/null 2>&1 || true + pactl set-card-profile alsa_card.platform-7702000.sound "HiFi (Earpiece, Mic1)" >/dev/null 2>&1 || true + wpctl set-mute @DEFAULT_AUDIO_SINK@ 0 >/dev/null 2>&1 || true + wpctl set-volume @DEFAULT_AUDIO_SINK@ 100% >/dev/null 2>&1 || true if command -v amixer >/dev/null 2>&1; then - amixer -c 0 sset 'SPK DAC Switch' off >/dev/null 2>&1 || true - amixer -c 0 sset 'RX3 MIX1 INP1' 'ZERO' >/dev/null 2>&1 || true - amixer -c 0 sset 'PRI_MI2S_RX Voice Mixer VoiceMMode1' on >/dev/null 2>&1 || true - amixer -c 0 sset 'VoiceMMode1 Capture Mixer TERT_MI2S_TX' on >/dev/null 2>&1 || true - amixer -c 0 sset 'MultiMedia2 Mixer TERT_MI2S_TX' on >/dev/null 2>&1 || true - amixer -c 0 sset 'RX1 MIX1 INP1' 'RX1' >/dev/null 2>&1 || true - amixer -c 0 sset 'RDAC2 MUX' 'RX1' >/dev/null 2>&1 || true - amixer -c 0 sset 'EAR_S' 'Switch' >/dev/null 2>&1 || true - amixer -c 0 sset 'CIC1 MUX' 'AMIC' >/dev/null 2>&1 || true - amixer -c 0 sset 'DEC1 MUX' 'ADC1' >/dev/null 2>&1 || true - amixer -c 0 sset 'ADC1 Volume' 6 >/dev/null 2>&1 || true - amixer -c 0 sset 'RX1 Digital Volume' 84 >/dev/null 2>&1 || true + amixer -c 0 cset "name='SPK DAC Switch'" 0 >/dev/null 2>&1 || true + amixer -c 0 cset "name='RX3 MIX1 INP1'" 'ZERO' >/dev/null 2>&1 || true + amixer -c 0 cset "name='RX3 Digital Volume'" 0 >/dev/null 2>&1 || true + amixer -c 0 cset "name='PRI_MI2S_RX Voice Mixer VoiceMMode1'" 1 >/dev/null 2>&1 || true + amixer -c 0 cset "name='VoiceMMode1 Capture Mixer TERT_MI2S_TX'" 1 >/dev/null 2>&1 || true + amixer -c 0 cset "name='MultiMedia2 Mixer TERT_MI2S_TX'" 0 >/dev/null 2>&1 || true + amixer -c 0 cset "name='RX1 MIX1 INP1'" 'RX1' >/dev/null 2>&1 || true + amixer -c 0 cset "name='RDAC2 MUX'" 'RX1' >/dev/null 2>&1 || true + amixer -c 0 cset "name='EAR_S'" 'Switch' >/dev/null 2>&1 || true + amixer -c 0 cset "name='RX1 Mute Switch'" 0 >/dev/null 2>&1 || true + amixer -c 0 cset "name='RX1 Digital Volume'" 88 >/dev/null 2>&1 || true + amixer -c 0 cset "name='CIC1 MUX'" 'AMIC' >/dev/null 2>&1 || true + amixer -c 0 cset "name='DEC1 MUX'" 'ADC1' >/dev/null 2>&1 || true + amixer -c 0 cset "name='ADC1 Volume'" 8 >/dev/null 2>&1 || true + amixer -c 0 cset "name='TX1 Digital Volume'" 88 >/dev/null 2>&1 || true fi sxmo_log "Enabled ALSA voice call audio mode (Earpiece)." sxmo_hook_statusbar.sh volume >/dev/null 2>&1 || true @@ -38,18 +45,21 @@ disable_call_audio_mode() { pactl set-card-profile alsa_card.platform-7702000.sound "HiFi (Mic1, Speaker)" >/dev/null 2>&1 || true fi if command -v amixer >/dev/null 2>&1; then - amixer -c 0 sset 'PRI_MI2S_RX Voice Mixer VoiceMMode1' off >/dev/null 2>&1 || true - amixer -c 0 sset 'VoiceMMode1 Capture Mixer TERT_MI2S_TX' off >/dev/null 2>&1 || true - amixer -c 0 sset 'EAR_S' 'ZERO' >/dev/null 2>&1 || true - amixer -c 0 sset 'RX1 MIX1 INP1' 'ZERO' >/dev/null 2>&1 || true - amixer -c 0 sset 'RX3 MIX1 INP1' 'RX1' >/dev/null 2>&1 || true + amixer -c 0 cset "name='PRI_MI2S_RX Voice Mixer VoiceMMode1'" 0 >/dev/null 2>&1 || true + amixer -c 0 cset "name='VoiceMMode1 Capture Mixer TERT_MI2S_TX'" 0 >/dev/null 2>&1 || true + amixer -c 0 cset "name='MultiMedia2 Mixer TERT_MI2S_TX'" 1 >/dev/null 2>&1 || true + amixer -c 0 cset "name='EAR_S'" 'ZERO' >/dev/null 2>&1 || true + amixer -c 0 cset "name='RX1 MIX1 INP1'" 'ZERO' >/dev/null 2>&1 || true + amixer -c 0 cset "name='RX1 Digital Volume'" 84 >/dev/null 2>&1 || true + amixer -c 0 cset "name='TX1 Digital Volume'" 84 >/dev/null 2>&1 || true + amixer -c 0 cset "name='RX3 MIX1 INP1'" 'RX1' >/dev/null 2>&1 || true if [ "$spk_vol" -gt 0 ]; then rx3_hw=$(( 60 + spk_vol * 32 / 100 )) - amixer -c 0 sset 'SPK DAC Switch' on >/dev/null 2>&1 || true - amixer -c 0 sset 'RX3 Digital Volume' "$rx3_hw" >/dev/null 2>&1 || true + amixer -c 0 cset "name='SPK DAC Switch'" 1 >/dev/null 2>&1 || true + amixer -c 0 cset "name='RX3 Digital Volume'" "$rx3_hw" >/dev/null 2>&1 || true else - amixer -c 0 sset 'SPK DAC Switch' off >/dev/null 2>&1 || true - amixer -c 0 sset 'RX3 Digital Volume' 0 >/dev/null 2>&1 || true + amixer -c 0 cset "name='SPK DAC Switch'" 0 >/dev/null 2>&1 || true + amixer -c 0 cset "name='RX3 Digital Volume'" 0 >/dev/null 2>&1 || true fi fi wpctl set-mute @DEFAULT_AUDIO_SINK@ "$([ "$spk_vol" -le 0 ] && echo 1 || echo 0)" >/dev/null 2>&1 || true diff --git a/overlay/rootfs/usr/bin/sxmo_vibrate b/overlay/rootfs/usr/bin/sxmo_vibrate index 20ce6dc..5241ff5 100755 Binary files a/overlay/rootfs/usr/bin/sxmo_vibrate and b/overlay/rootfs/usr/bin/sxmo_vibrate differ diff --git a/overlay/rootfs/usr/lib/dwm/plugins/callscreen.so b/overlay/rootfs/usr/lib/dwm/plugins/callscreen.so index 116ba58..675314a 100755 Binary files a/overlay/rootfs/usr/lib/dwm/plugins/callscreen.so and b/overlay/rootfs/usr/lib/dwm/plugins/callscreen.so differ diff --git a/overlay/rootfs/usr/lib/dwm/plugins/networks.so b/overlay/rootfs/usr/lib/dwm/plugins/networks.so index a0163b0..5c677cd 100755 Binary files a/overlay/rootfs/usr/lib/dwm/plugins/networks.so and b/overlay/rootfs/usr/lib/dwm/plugins/networks.so differ diff --git a/overlay/rootfs/usr/lib/dwm/plugins/ringtones.so b/overlay/rootfs/usr/lib/dwm/plugins/ringtones.so index 6c0fe6c..03ddca6 100755 Binary files a/overlay/rootfs/usr/lib/dwm/plugins/ringtones.so and b/overlay/rootfs/usr/lib/dwm/plugins/ringtones.so differ diff --git a/overlay/rootfs/usr/lib/dwm/plugins/xdrawer.so b/overlay/rootfs/usr/lib/dwm/plugins/xdrawer.so index 674cf68..6215adb 100755 Binary files a/overlay/rootfs/usr/lib/dwm/plugins/xdrawer.so and b/overlay/rootfs/usr/lib/dwm/plugins/xdrawer.so differ diff --git a/overlay/rootfs/usr/sbin/ModemManager b/overlay/rootfs/usr/sbin/ModemManager index ed356e3..aeaa1a5 100755 Binary files a/overlay/rootfs/usr/sbin/ModemManager and b/overlay/rootfs/usr/sbin/ModemManager differ diff --git a/overlay/rootfs/usr/share/alsa/ucm2/codecs/msm8916-wcd/Earpiece.conf b/overlay/rootfs/usr/share/alsa/ucm2/codecs/msm8916-wcd/Earpiece.conf new file mode 100644 index 0000000..feda03f --- /dev/null +++ b/overlay/rootfs/usr/share/alsa/ucm2/codecs/msm8916-wcd/Earpiece.conf @@ -0,0 +1,27 @@ +SectionDevice."Earpiece" { + Comment "Earpiece playback" + + ConflictingDevice [ + "Headphones" + ] + + EnableSequence [ + cset "name='RX1 MIX1 INP1' RX1" + cset "name='RDAC2 MUX' RX1" + cset "name='EAR_S' 1" + ## gain to +6dB + cset "name='RX1 Digital Volume' 90" + ] + + DisableSequence [ + cset "name='RX1 Digital Volume' 0" + cset "name='EAR_S' 0" + cset "name='RX1 MIX1 INP1' ZERO" + ] + + Value { + PlaybackPCM "${var:WcdPlaybackPCM}" + PlaybackChannels 2 + PlaybackPriority 100 + } +} diff --git a/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_ring.sh b/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_ring.sh index 1d95248..0f60d94 100755 --- a/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_ring.sh +++ b/overlay/rootfs/usr/share/sxmo/default_hooks/sxmo_hook_ring.sh @@ -4,13 +4,14 @@ # Copyright 2022 Sxmo Contributors # This script is executed (asynchronously) when you get an incoming call -# You can use it to play a ring tone - # $1 = Contact Name or Number (if not in contacts) # shellcheck source=scripts/core/sxmo_common.sh . sxmo_common.sh +export XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-${HOME:-/home/user}/.config}" +export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}" + if [ -z "${SXMO_VIBRATE_DEV:-}" ] && [ ! -e /dev/input/by-path/platform-vibrator-event ]; then for vdev in /dev/input/by-path/*vibrator*-event; do if [ -e "$vdev" ]; then @@ -25,33 +26,61 @@ if [ -f "$XDG_CONFIG_HOME/sxmo/ringsettings" ]; then . "$XDG_CONFIG_HOME/sxmo/ringsettings" fi -# pm8xxx-vib shifts FF_RUMBLE strong_magnitude right by 8 bits (valid 256..65535) -case "${SXMO_VIBRATE_STRENGTH:-}" in - 1) SXMO_VIBRATE_STRENGTH=8000 ;; - 2|"") SXMO_VIBRATE_STRENGTH=32000 ;; - 3) SXMO_VIBRATE_STRENGTH=65535 ;; +case "${SXMO_VIBRATE_STRENGTH:-2}" in + 1) SXMO_VIBRATE_STRENGTH=1 ;; + 3) SXMO_VIBRATE_STRENGTH=3 ;; + *) + if [ "${SXMO_VIBRATE_STRENGTH:-2}" -gt 45000 ] 2>/dev/null; then + SXMO_VIBRATE_STRENGTH=3 + elif [ "${SXMO_VIBRATE_STRENGTH:-2}" -gt 3 ] && [ "${SXMO_VIBRATE_STRENGTH:-2}" -le 16000 ] 2>/dev/null; then + SXMO_VIBRATE_STRENGTH=1 + else + SXMO_VIBRATE_STRENGTH=2 + fi + ;; esac +export SXMO_VIBRATE_STRENGTH if [ -f "$XDG_CONFIG_HOME/sxmo/ringtone" ]; then - custom_ring="$(head -n 1 "$XDG_CONFIG_HOME/sxmo/ringtone" 2>/dev/null)" + custom_ring="$(head -n 1 "$XDG_CONFIG_HOME/sxmo/ringtone" 2>/dev/null | tr -d '\r')" if [ -n "$custom_ring" ] && [ -f "$custom_ring" ]; then SXMO_RINGTONE="$custom_ring" fi fi -# Only vibrate if you already got an active call -if sxmo_modemcall.sh list_active_calls \ - | grep -v ringing-in \ - | grep -q .; then - sxmo_vibrate 1500 "${SXMO_VIBRATE_STRENGTH:-1}" - exit +if [ -z "${SXMO_RINGTONE:-}" ] || [ ! -f "${SXMO_RINGTONE:-}" ]; then + if [ -f /usr/share/sxmo/ringtones/Banana_Chime.wav ]; then + SXMO_RINGTONE="/usr/share/sxmo/ringtones/Banana_Chime.wav" + elif [ -f /usr/share/sxmo/ring.ogg ]; then + SXMO_RINGTONE="/usr/share/sxmo/ring.ogg" + else + for f in /usr/share/sxmo/ringtones/*; do + if [ -f "$f" ]; then + SXMO_RINGTONE="$f" + break + fi + done + fi +fi +export SXMO_RINGTONE + +# DND / BUSY MODE +if [ -f "$XDG_RUNTIME_DIR/sxmo.busy" ]; then + exit 0 +fi +if [ -f "$XDG_CONFIG_HOME/sxmo/.busy" ]; then + end="$(cat "$XDG_CONFIG_HOME/sxmo/.busy" 2>/dev/null)" + if [ -z "$end" ] || [ "$(date +%s)" -lt "$end" ]; then + exit 0 + fi fi -# Shallow if you have more than one ringing call -if ! sxmo_modemcall.sh list_active_calls \ - | grep -c ringing-in \ - | grep -q 1; then - exit +# Only vibrate once if a call is already active +if [ "$(cat /tmp/sxmo_active_call 2>/dev/null)" = "2" ]; then + if [ ! -f "$XDG_CONFIG_HOME/sxmo/.novibrate" ]; then + sxmo_vibrate 1500 "$SXMO_VIBRATE_STRENGTH" + fi + exit 0 fi case "${SXMO_RINGVOL:-100}" in @@ -59,90 +88,167 @@ case "${SXMO_RINGVOL:-100}" in esac [ "$SXMO_RINGVOL" -gt 100 ] && SXMO_RINGVOL=100 -SAVED_RX3="$(amixer -c 0 cget "name='RX3 Digital Volume'" 2>/dev/null | awk -F= '/: values=/ {print $2; exit}')" -SAVED_SPK_VOL=100 +SAVED_SPK_VOL=85 if [ -r "$XDG_CONFIG_HOME/sxmo/speaker_vol" ]; then - read -r SAVED_SPK_VOL < "$XDG_CONFIG_HOME/sxmo/speaker_vol" 2>/dev/null || SAVED_SPK_VOL=100 + read -r SAVED_SPK_VOL < "$XDG_CONFIG_HOME/sxmo/speaker_vol" 2>/dev/null || SAVED_SPK_VOL=85 fi -case "$SAVED_SPK_VOL" in ''|*[!0-9]*) SAVED_SPK_VOL=100 ;; esac +case "$SAVED_SPK_VOL" in ''|*[!0-9]*) SAVED_SPK_VOL=85 ;; esac +SAVED_RX3=$(( 60 + SAVED_SPK_VOL * 32 / 100 )) + +RING_PID="" +VIB_PID="" + +is_call_picked_up() { + [ "$(cat /tmp/sxmo_active_call 2>/dev/null)" = "2" ] && return 0 + for f in "$XDG_RUNTIME_DIR"/sxmo_calls/*.pickedupcall "$XDG_RUNTIME_DIR"/sxmo_calls/*.initiatedcall; do + [ -e "$f" ] && return 0 + done + return 1 +} finish() { trap - INT TERM EXIT - jobs -p | xargs -r kill 2>/dev/null || true - wait 2>/dev/null || true - if [ -n "${SAVED_RX3:-}" ]; then + [ -n "$RING_PID" ] && pkill -TERM -P "$RING_PID" 2>/dev/null || true + [ -n "$VIB_PID" ] && pkill -TERM -P "$VIB_PID" 2>/dev/null || true + [ -n "$RING_PID" ] && kill -TERM "$RING_PID" 2>/dev/null || true + [ -n "$VIB_PID" ] && kill -TERM "$VIB_PID" 2>/dev/null || true + pkill -TERM -P "$$" 2>/dev/null || true + if ! is_call_picked_up; then amixer -c 0 cset "name='RX3 Digital Volume'" "$SAVED_RX3" >/dev/null 2>&1 || true + wpctl set-volume @DEFAULT_AUDIO_SINK@ "${SAVED_SPK_VOL}%" >/dev/null 2>&1 || true fi - wpctl set-volume @DEFAULT_AUDIO_SINK@ "${SAVED_SPK_VOL}%" >/dev/null 2>&1 || true - exit + exit 0 +} + +pause_media_async() { + if [ -p /tmp/dwm_plugin_music.fifo ]; then + echo "PAUSE" > /tmp/dwm_plugin_music.fifo 2>/dev/null || true + fi + mpc -q pause >/dev/null 2>&1 & + sxmo_playerctl.sh pause_all >/dev/null 2>&1 & } ring() { - if [ "${SXMO_RINGVOL:-100}" -le 0 ]; then + if [ "${SXMO_RINGVOL:-100}" -le 0 ] || [ -z "${SXMO_RINGTONE:-}" ] || [ ! -f "$SXMO_RINGTONE" ]; then return 0 fi ring_rx3=$(( 60 + SXMO_RINGVOL * 34 / 100 )) - mpv_vol=$(( SXMO_RINGVOL * 130 / 100 )) - amixer -c 0 cset "name='SPK DAC Switch'" 1 >/dev/null 2>&1 || true - amixer -c 0 cset "name='RX3 MIX1 INP1'" 3 >/dev/null 2>&1 || true - amixer -c 0 cset "name='RX3 Digital Volume'" "$ring_rx3" >/dev/null 2>&1 || true - wpctl set-mute @DEFAULT_AUDIO_SINK@ 0 >/dev/null 2>&1 || true - wpctl set-volume @DEFAULT_AUDIO_SINK@ 100% >/dev/null 2>&1 || true - mpv --no-resume-playback --quiet --no-video \ - --ao=pipewire,pulse,alsa --audio-channels=stereo --audio-samplerate=48000 \ - --volume-max=150 --volume="$mpv_vol" \ - --loop="${SXMO_RINGNUMBER:-10}" "$SXMO_RINGTONE" >/dev/null & + pw_vol="$(awk -v v="$SXMO_RINGVOL" 'BEGIN { printf "%.2f", v / 100.0 }')" + + ( + rchild="" + vhelper="" + trap '[ -n "$rchild" ] && kill -TERM "$rchild" 2>/dev/null; [ -n "$vhelper" ] && kill -TERM "$vhelper" 2>/dev/null; exit 0' INT TERM EXIT + + amixer -c 0 -s >/dev/null 2>&1 </dev/null 2>&1 || true + wpctl set-volume @DEFAULT_AUDIO_SINK@ "${SXMO_RINGVOL}%" >/dev/null 2>&1 || true + + ( + for d in 0.15 0.40; do + sleep "$d" || exit 0 + is_call_picked_up && exit 0 + amixer -c 0 -s >/dev/null 2>&1 </dev/null 2>&1 || true + done + ) & + vhelper="$!" + + ext="$(printf '%s' "${SXMO_RINGTONE##*.}" | tr '[:upper:]' '[:lower:]')" + use_pw=0 + case "$ext" in + wav|ogg|oga|mp3|flac|aiff|aif) + command -v pw-play >/dev/null 2>&1 && use_pw=1 + ;; + esac + + loops="${SXMO_RINGNUMBER:-10}" + if [ "$use_pw" -eq 1 ]; then + i=0 + while [ "$i" -lt "$loops" ]; do + is_call_picked_up && break + pw-play --volume="$pw_vol" "$SXMO_RINGTONE" >/dev/null 2>&1 & + rchild="$!" + wait "$rchild" 2>/dev/null + rc="$?" + rchild="" + if [ "$rc" -ne 0 ]; then + use_pw=0 + break + fi + i=$(( i + 1 )) + sleep 0.2 & + rchild="$!" + wait "$rchild" 2>/dev/null || break + rchild="" + done + fi + + if [ "$use_pw" -eq 0 ] && ! is_call_picked_up; then + mpv --no-config --no-terminal --no-resume-playback --quiet --vo=null --no-video \ + --ao=pipewire,pulse,alsa --audio-channels=stereo --audio-samplerate=48000 \ + --volume-max=150 --volume="$SXMO_RINGVOL" \ + --loop="$loops" "$SXMO_RINGTONE" /dev/null 2>&1 & + rchild="$!" + wait "$rchild" 2>/dev/null || true + rchild="" + fi + ) & + RING_PID="$!" } vibrate() { - while : ; do - trap 'finish' INT TERM EXIT - sxmo_vibrate 1500 "${SXMO_VIBRATE_STRENGTH:-1}" & - wait "$!" + ( + vchild="" + trap '[ -n "$vchild" ] && kill -TERM "$vchild" 2>/dev/null; exit 0' INT TERM EXIT + while : ; do + is_call_picked_up && exit 0 + sxmo_vibrate 1500 "$SXMO_VIBRATE_STRENGTH" & + vchild="$!" + wait "$vchild" 2>/dev/null || exit 0 + vchild="" - sleep 0.5 & - wait "$!" - done & + sleep 0.5 & + vchild="$!" + wait "$vchild" 2>/dev/null || exit 0 + vchild="" + done + ) & + VIB_PID="$!" } -# SILENT MODE -if [ -f "$XDG_CONFIG_HOME"/sxmo/.busy ]; then - end="$(cat "$XDG_CONFIG_HOME"/sxmo/.busy)" - if [ -z "$end" ] || [ "$(date +%s)" -lt "$end" ]; then - exit - fi -fi +trap 'finish' INT TERM EXIT # RING & VIBRATE MODE (DEFAULT) -if [ ! -f "$XDG_CONFIG_HOME"/sxmo/.noring ] && [ ! -f "$XDG_CONFIG_HOME"/sxmo/.novibrate ]; then - sxmo_log "RING AND VIBRATE" - - sxmo_playerctl.sh pause_all - if [ -p /tmp/dwm_plugin_music.fifo ]; then - echo "PAUSE" > /tmp/dwm_plugin_music.fifo 2>/dev/null || true - fi - - ring +if [ ! -f "$XDG_CONFIG_HOME/sxmo/.noring" ] && [ ! -f "$XDG_CONFIG_HOME/sxmo/.novibrate" ]; then + sxmo_log "RING AND VIBRATE ($SXMO_RINGTONE, vol=${SXMO_RINGVOL}%, vib=${SXMO_VIBRATE_STRENGTH})" + pause_media_async vibrate + ring # RING-ONLY MODE -elif [ ! -f "$XDG_CONFIG_HOME"/sxmo/.noring ] && [ -f "$XDG_CONFIG_HOME"/sxmo/.novibrate ]; then - sxmo_log "RING ONLY" - - sxmo_playerctl.sh pause_all - if [ -p /tmp/dwm_plugin_music.fifo ]; then - echo "PAUSE" > /tmp/dwm_plugin_music.fifo 2>/dev/null || true - fi - +elif [ ! -f "$XDG_CONFIG_HOME/sxmo/.noring" ] && [ -f "$XDG_CONFIG_HOME/sxmo/.novibrate" ]; then + sxmo_log "RING ONLY ($SXMO_RINGTONE, vol=${SXMO_RINGVOL}%)" + pause_media_async ring # VIBRATE-ONLY MODE -elif [ ! -f "$XDG_CONFIG_HOME"/sxmo/.novibrate ] && [ -f "$XDG_CONFIG_HOME"/sxmo/.noring ]; then - sxmo_log "VIBRATE ONLY" - +elif [ ! -f "$XDG_CONFIG_HOME/sxmo/.novibrate" ] && [ -f "$XDG_CONFIG_HOME/sxmo/.noring" ]; then + sxmo_log "VIBRATE ONLY (vib=${SXMO_VIBRATE_STRENGTH})" vibrate fi -trap 'finish' INT TERM EXIT sleep "${SXMO_RINGTIME:-20}" & wait "$!" +finish diff --git a/prebuilt/sxmo-x11/usr/bin/sxmo_vibrate b/prebuilt/sxmo-x11/usr/bin/sxmo_vibrate index 20ce6dc..5241ff5 100755 Binary files a/prebuilt/sxmo-x11/usr/bin/sxmo_vibrate and b/prebuilt/sxmo-x11/usr/bin/sxmo_vibrate differ diff --git a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/callscreen.so b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/callscreen.so index 116ba58..675314a 100755 Binary files a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/callscreen.so and b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/callscreen.so differ diff --git a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/networks.so b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/networks.so index a0163b0..5c677cd 100755 Binary files a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/networks.so and b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/networks.so differ diff --git a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/ringtones.so b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/ringtones.so index 6c0fe6c..03ddca6 100755 Binary files a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/ringtones.so and b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/ringtones.so differ diff --git a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/xdrawer.so b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/xdrawer.so index 674cf68..6215adb 100755 Binary files a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/xdrawer.so and b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/xdrawer.so differ diff --git a/src/dwm-plugins/callscreen_plugin.cpp b/src/dwm-plugins/callscreen_plugin.cpp index 65bfbcf..8da4e0f 100644 --- a/src/dwm-plugins/callscreen_plugin.cpp +++ b/src/dwm-plugins/callscreen_plugin.cpp @@ -192,10 +192,12 @@ std::string sanitizeForFilename(const std::string& raw) { } } if (out.empty()) { - return "unknown"; + return "voice"; } - if (out.size() > 20) { - out = out.substr(0, 20); + if (out.size() > 10 && out[0] == '+') { + out = out.substr(out.size() - 10); + } else if (out.size() > 10) { + out = out.substr(0, 10); } return out; } @@ -217,7 +219,7 @@ void finalizeWavHeader(const std::string& path) { return; } uint8_t hdr[64] = {0}; - ssize_t n = :: pread(fd, hdr, sizeof(hdr), 0); + ssize_t n = ::pread(fd, hdr, sizeof(hdr), 0); if (n < 44 || std::memcmp(hdr, "RIFF", 4) != 0 || std::memcmp(hdr + 8, "WAVE", 4) != 0) { ::close(fd); @@ -237,6 +239,12 @@ void finalizeWavHeader(const std::string& path) { }; (void)::pwrite(fd, b, 4, 4); } + ssize_t dataOff = -1; + uint32_t dataBytes = 0; + uint16_t audioFmt = static_cast(hdr[20]) | + (static_cast(hdr[21]) << 8); + uint16_t bitsPerSample = static_cast(hdr[34]) | + (static_cast(hdr[35]) << 8); for (ssize_t off = 12; off + 8 <= n;) { uint32_t chunkLen = static_cast(hdr[off + 4]) | (static_cast(hdr[off + 5]) << 8) | @@ -244,6 +252,8 @@ void finalizeWavHeader(const std::string& path) { (static_cast(hdr[off + 7]) << 24); if (std::memcmp(hdr + off, "data", 4) == 0) { uint32_t expectedData = static_cast(st.st_size - (off + 8)); + dataOff = off + 8; + dataBytes = expectedData; if (chunkLen == 0 || chunkLen != expectedData) { uint8_t b[4] = { static_cast(expectedData & 0xff), @@ -258,6 +268,33 @@ void finalizeWavHeader(const std::string& path) { if (chunkLen == 0) break; off += 8 + static_cast((chunkLen + 1) & ~1u); } + // Peak-normalize quiet 16-bit signed PCM recordings so playback is clearly audible + if (audioFmt == 1 && bitsPerSample == 16 && dataOff >= 44 && + dataBytes >= 2 && dataBytes <= 32 * 1024 * 1024) { + size_t numSamples = dataBytes / 2; + std::vector pcm(numSamples); + ssize_t rd = ::pread(fd, pcm.data(), numSamples * sizeof(int16_t), dataOff); + if (rd == static_cast(numSamples * sizeof(int16_t))) { + int maxAbs = 0; + size_t skip = std::min(numSamples / 4, 400); + for (size_t i = skip; i < numSamples; ++i) { + int v = std::abs(static_cast(pcm[i])); + if (v > maxAbs) { + maxAbs = v; + } + } + if (maxAbs >= 32 && maxAbs < 18000) { + double gain = std::min(24.0, 24000.0 / static_cast(maxAbs)); + if (gain > 1.15) { + for (size_t i = 0; i < numSamples; ++i) { + int scaled = static_cast(std::lround(pcm[i] * gain)); + pcm[i] = static_cast(std::clamp(scaled, -32767, 32767)); + } + (void)::pwrite(fd, pcm.data(), numSamples * sizeof(int16_t), dataOff); + } + } + } + } ::fsync(fd); ::close(fd); } @@ -434,7 +471,7 @@ int callscreen_run_main(int argc, char** argv) { bool ringMuted = false; bool micMuted = false; bool speakerOn = false; - int callVolumePct = 80; + int callVolumePct = 85; int selectedRow = 0; int64_t callStartMs = 0; @@ -446,6 +483,8 @@ int callscreen_run_main(int argc, char** argv) { int64_t recordStartMs = 0; std::string recordFilePath; std::string recordFileName; + int lastRecordedSec = 0; + int recordedClipCount = 0; bool visible = false; int64_t openStartMs = 0; @@ -454,6 +493,22 @@ int callscreen_run_main(int argc, char** argv) { int softkeyPending = 0; int64_t softkeyDeadlineMs = 0; + auto pctToHwVol = [](int pct, bool spk) -> long { + int c = std::clamp(pct, 5, 100); + // RX1/RX3 Digital Volume: 84 = 0 dB, 88 = +4 dB, 92 = +8 dB, 94 = +10 dB + return spk ? (68 + (c * 26) / 100) : (68 + (c * 24) / 100); + }; + + auto hwVolToPct = [](long hwVol, bool spk) -> int { + if (hwVol < 68) { + return 85; + } + int span = spk ? 26 : 24; + int pct = static_cast(std::lround(((hwVol - 68) * 100.0) / span)); + pct = std::clamp(pct, 5, 100); + return std::max(5, ((pct + 2) / 5) * 5); + }; + auto refreshHardwareAudioState = [&]() { unsigned int dec1 = alsa.getEnum("DEC1 MUX", 1); if (dec1 > 0) { @@ -464,27 +519,18 @@ int callscreen_run_main(int argc, char** argv) { savedMicMux = static_cast(std::max(1, readIntFile("/tmp/xdrawer_mic_mux", 1))); micMuted = true; } - bool spkSwitch = alsa.getBoolean("SPK DAC Switch", false); - bool earSwitch = (alsa.getEnum("EAR_S", 1) != 0); - speakerOn = (spkSwitch && !earSwitch); - - long rawVol = speakerOn ? alsa.getInteger("RX3 Digital Volume", 68) - : alsa.getInteger("RX1 Digital Volume", 68); - if (rawVol <= 0) { - rawVol = alsa.getInteger("RX1 Digital Volume", 68); - } - callVolumePct = std::clamp(static_cast(std::lround((rawVol * 100.0) / 84.0)), 0, 100); - callVolumePct = (callVolumePct / 5) * 5; - if (callVolumePct == 0) { - callVolumePct = 75; + int savedVol = readIntFile("/tmp/sxmo_call_vol", -1); + if (savedVol >= 5 && savedVol <= 100) { + callVolumePct = savedVol; } }; auto setMicMuted = [&](bool mute) { micMuted = mute; alsa.setEnum("DEC1 MUX", mute ? 0 : (savedMicMux > 0 ? savedMicMux : 1)); - alsa.setInteger("ADC1 Volume", mute ? 0 : 6); - alsa.setInteger("ADC3 Volume", mute ? 0 : 6); + alsa.setInteger("ADC1 Volume", mute ? 0 : 8); + alsa.setInteger("ADC3 Volume", mute ? 0 : 8); + alsa.setInteger("TX1 Digital Volume", mute ? 0 : 88); alsa.setBoolean("VoiceMMode1 Capture Mixer TERT_MI2S_TX", !mute); runAsyncCmd(mute ? "wpctl set-mute @DEFAULT_AUDIO_SOURCE@ 1 >/dev/null 2>&1" : "wpctl set-mute @DEFAULT_AUDIO_SOURCE@ 0 >/dev/null 2>&1"); @@ -494,7 +540,15 @@ int callscreen_run_main(int argc, char** argv) { auto setSpeakerRoute = [&](bool toSpeaker) { speakerOn = toSpeaker; - long hwVol = std::lround((callVolumePct * 84.0) / 100.0); + long hwVol = pctToHwVol(callVolumePct, toSpeaker); + alsa.setBoolean("PRI_MI2S_RX Voice Mixer VoiceMMode1", true); + alsa.setBoolean("VoiceMMode1 Capture Mixer TERT_MI2S_TX", !micMuted); + alsa.setEnum("CIC1 MUX", 0); + if (!micMuted) { + alsa.setEnum("DEC1 MUX", savedMicMux > 0 ? savedMicMux : 1); + alsa.setInteger("ADC1 Volume", 8); + alsa.setInteger("TX1 Digital Volume", 88); + } if (toSpeaker) { alsa.setEnum("EAR_S", 0); alsa.setEnum("RX1 MIX1 INP1", 0); @@ -505,9 +559,11 @@ int callscreen_run_main(int argc, char** argv) { } else { alsa.setBoolean("SPK DAC Switch", false); alsa.setEnum("RX3 MIX1 INP1", 0); + alsa.setInteger("RX3 Digital Volume", 0); alsa.setEnum("RX1 MIX1 INP1", 3); alsa.setEnum("RDAC2 MUX", 0); alsa.setEnum("EAR_S", 1); + alsa.setBoolean("RX1 Mute Switch", false); alsa.setInteger("RX1 Digital Volume", hwVol); statusNotice = "Audio routed to Earpiece"; } @@ -516,10 +572,10 @@ int callscreen_run_main(int argc, char** argv) { auto adjustVolume = [&](int deltaPct) { callVolumePct = std::clamp(callVolumePct + deltaPct, 5, 100); - long hwVol = std::lround((callVolumePct * 84.0) / 100.0); - alsa.setInteger("RX1 Digital Volume", hwVol); + writeIntFile("/tmp/sxmo_call_vol", callVolumePct); + alsa.setInteger("RX1 Digital Volume", pctToHwVol(callVolumePct, false)); if (speakerOn) { - alsa.setInteger("RX3 Digital Volume", hwVol); + alsa.setInteger("RX3 Digital Volume", pctToHwVol(callVolumePct, true)); } char cmd[128]; std::snprintf(cmd, sizeof(cmd), @@ -528,6 +584,24 @@ int callscreen_run_main(int argc, char** argv) { runAsyncCmd(cmd); }; + auto finishRecordingFile = [&]() { + alsa.setBoolean("MultiMedia2 Mixer TERT_MI2S_TX", false); + finalizeWavHeader(recordFilePath); + struct stat st {}; + if (!recordFilePath.empty() && ::stat(recordFilePath.c_str(), &st) == 0 && + st.st_size > 44) { + int durFromBytes = static_cast((st.st_size - 44 + 8000) / 16000); + int durFromWall = static_cast((nowMs() - recordStartMs + 500) / 1000); + lastRecordedSec = std::max(1, std::max(durFromBytes, durFromWall)); + ++recordedClipCount; + statusNotice = "SAVED (" + formatTimerSec(lastRecordedSec) + ") " + recordFileName; + statusNoticeUntilMs = nowMs() + 8000; + } else if (!recordFileName.empty()) { + statusNotice = "Recording failed"; + statusNoticeUntilMs = nowMs() + 4000; + } + }; + auto checkRecordingAlive = [&]() -> bool { if (recordPid <= 0) { return false; @@ -536,7 +610,7 @@ int callscreen_run_main(int argc, char** argv) { pid_t r = ::waitpid(recordPid, &status, WNOHANG); if (r == recordPid || (r < 0 && ::kill(recordPid, 0) != 0)) { recordPid = -1; - finalizeWavHeader(recordFilePath); + finishRecordingFile(); return false; } return true; @@ -565,11 +639,7 @@ int callscreen_run_main(int argc, char** argv) { } recordPid = -1; } - finalizeWavHeader(recordFilePath); - if (!recordFileName.empty()) { - statusNotice = "Saved: " + recordFileName; - statusNoticeUntilMs = nowMs() + 3500; - } + finishRecordingFile(); }; auto startRecording = [&]() -> bool { @@ -581,18 +651,19 @@ int callscreen_run_main(int argc, char** argv) { struct tm tmNow {}; localtime_r(&nowSec, &tmNow); char tsBuf[32]; - std::strftime(tsBuf, sizeof(tsBuf), "%Y%m%d_%H%M%S", &tmNow); + std::strftime(tsBuf, sizeof(tsBuf), "%H%M%S", &tmNow); std::string tag = sanitizeForFilename( !contactName.empty() ? contactName : (!callNumber.empty() ? callNumber : "voice")); - recordFileName = "call_" + tag + "_" + tsBuf + ".wav"; + recordFileName = std::string("rec_") + tsBuf + "_" + tag + ".wav"; recordFilePath = dir + "/" + recordFileName; - // Ensure MultiMedia2 capture mixer and primary mic path are active for PipeWire recording + // Enable MultiMedia2 capture mixer at 8 kHz mono to match active VoiceMMode1 AFE port alsa.setEnum("CIC1 MUX", 0); if (!micMuted) { alsa.setEnum("DEC1 MUX", savedMicMux > 0 ? savedMicMux : 1); - alsa.setInteger("ADC1 Volume", 6); + alsa.setInteger("ADC1 Volume", 8); + alsa.setInteger("TX1 Digital Volume", 88); } alsa.setBoolean("MultiMedia2 Mixer TERT_MI2S_TX", true); @@ -602,12 +673,6 @@ int callscreen_run_main(int argc, char** argv) { } if (pid == 0) { ::setsid(); - std::string runtimeDir = "/run/user/" + std::to_string(::getuid()); - const char* xdg = std::getenv("XDG_RUNTIME_DIR"); - struct stat stRuntime {}; - if (!xdg || !*xdg || ::stat(xdg, &stRuntime) != 0) { - ::setenv("XDG_RUNTIME_DIR", runtimeDir.c_str(), 1); - } int nullfd = ::open("/dev/null", O_RDWR); if (nullfd >= 0) { ::dup2(nullfd, STDIN_FILENO); @@ -617,19 +682,17 @@ int callscreen_run_main(int argc, char** argv) { ::close(nullfd); } } - execlp("pw-record", "pw-record", "--format", "s16", "--rate", "16000", - "--channels", "1", recordFilePath.c_str(), + execlp("arecord", "arecord", "-q", "-D", "hw:0,1", "-f", "S16_LE", + "-r", "8000", "-c", "1", recordFilePath.c_str(), static_cast(nullptr)); - execlp("parecord", "parecord", "--rate=16000", "--channels=1", - "--file-format=wav", recordFilePath.c_str(), static_cast(nullptr)); - execlp("arecord", "arecord", "-f", "S16_LE", "-r", "16000", "-c", "1", + execlp("arecord", "arecord", "-q", "-f", "S16_LE", "-r", "8000", "-c", "1", recordFilePath.c_str(), static_cast(nullptr)); _exit(1); } recordPid = pid; recordStartMs = nowMs(); - statusNotice = "Recording to " + recordFileName; - statusNoticeUntilMs = nowMs() + 2500; + statusNotice = "REC -> " + recordFileName; + statusNoticeUntilMs = nowMs() + 3000; return true; }; @@ -804,9 +867,17 @@ int callscreen_run_main(int argc, char** argv) { drawPango(layoutSmall, &colLight, 8, 88, 26, "[Call] Answer [*] Mute [End] Reject", 224); } else if (callState == CallState::Ended) { - drawPango(layoutSmall, &colMuted, 8, 88, 26, - recordFileName.empty() ? "Call finished" : ("Saved: " + recordFileName), - 224); + if (recordedClipCount > 0 && !recordFileName.empty()) { + std::string savedStr = "SAVED (" + formatTimerSec(lastRecordedSec) + ") " + + recordFileName; + drawPango(layoutSmall, &colGreen, 8, 88, 26, savedStr, 224); + } else { + drawPango(layoutSmall, &colMuted, 8, 88, 26, "Call finished", 224); + } + } else if (recordedClipCount > 0 && !recordFileName.empty()) { + std::string savedStr = "SAVED (" + formatTimerSec(lastRecordedSec) + ") " + + recordFileName; + drawPango(layoutSmall, &colGreen, 8, 88, 26, savedStr, 224); } else { drawPango(layoutSmall, &colMuted, 8, 88, 26, "[1] Mic [2] Rec [3] Route [0] DTMF", 224); @@ -883,16 +954,21 @@ int callscreen_run_main(int argc, char** argv) { "[1] Microphone", std::max(80, kWinWidth - vw - 48)); drawPango(layoutMain, icCol, kWinWidth - vw - 8, ry, rowH, valStr); } else if (i == 1) { - XftColor* icCol = isRec ? &colRed : &colMuted; - std::string valStr = isRec - ? ("REC " + formatTimerSec(static_cast((now - recordStartMs) / 1000))) - : "OFF"; + XftColor* icCol = isRec ? &colRed + : (recordedClipCount > 0 ? &colGreen : &colMuted); + std::string valStr; + if (isRec) { + valStr = "REC " + formatTimerSec(static_cast((now - recordStartMs) / 1000)); + } else if (recordedClipCount > 0) { + valStr = "SAVED " + formatTimerSec(lastRecordedSec); + } else { + valStr = "OFF"; + } int vw = pangoWidth(layoutMain, valStr); drawPango(layoutIcon, icCol, 10, ry, rowH, kIconRecord); drawPango(layoutMain, isSel ? &colWhite : &colLight, 34, ry, rowH, - "[2] Record", std::max(80, kWinWidth - vw - 44)); - drawPango(layoutMain, isRec ? &colRed : &colMuted, - kWinWidth - vw - 8, ry, rowH, valStr); + "[2] Record", std::max(72, kWinWidth - vw - 44)); + drawPango(layoutMain, icCol, kWinWidth - vw - 8, ry, rowH, valStr); } else if (i == 2) { const char* ic = speakerOn ? kIconSpeaker : kIconEarpiece; XftColor* icCol = speakerOn ? &colYellow : &colBlue; @@ -1242,9 +1318,13 @@ int callscreen_run_main(int argc, char** argv) { } } if (foundState == "active" && callState != CallState::Active) { + if (callState != CallState::Outgoing) { + speakerOn = false; + } callState = CallState::Active; callStartMs = nowMs(); ringMuted = false; + setSpeakerRoute(speakerOn); writeActiveStateFiles(); render(); } else if (foundState == "ringing-in" && callState == CallState::Idle) { @@ -1359,10 +1439,15 @@ int callscreen_run_main(int argc, char** argv) { callId.clear(); callNumber.clear(); contactName.clear(); + recordFileName.clear(); + recordFilePath.clear(); + lastRecordedSec = 0; + recordedClipCount = 0; updateCallerDetails(arg1, arg2); callState = CallState::Incoming; callStartMs = nowMs(); ringMuted = false; + speakerOn = false; dtmfMode = false; dtmfSent.clear(); selectedRow = 0; @@ -1372,24 +1457,39 @@ int callscreen_run_main(int argc, char** argv) { callId.clear(); callNumber.clear(); contactName.clear(); + recordFileName.clear(); + recordFilePath.clear(); + lastRecordedSec = 0; + recordedClipCount = 0; updateCallerDetails(arg1, arg2); callState = CallState::Outgoing; callStartMs = nowMs(); ringMuted = false; + speakerOn = false; dtmfMode = false; dtmfSent.clear(); selectedRow = 0; + setSpeakerRoute(false); + statusNotice.clear(); + statusNoticeUntilMs = 0; writeActiveStateFiles(); showWindow(); } else if (verb == "ACTIVE") { bool wasActive = (callState == CallState::Active); + bool wasOutgoing = (callState == CallState::Outgoing); updateCallerDetails(arg1, arg2); callState = CallState::Active; if (!wasActive) { callStartMs = nowMs(); + if (!wasOutgoing) { + speakerOn = false; + } } ringMuted = false; selectedRow = 0; + setSpeakerRoute(speakerOn); + statusNotice.clear(); + statusNoticeUntilMs = 0; writeActiveStateFiles(); showWindow(); } else if (verb == "OPEN") { diff --git a/src/dwm-plugins/networks_plugin.cpp b/src/dwm-plugins/networks_plugin.cpp index 2614bd6..5f9c2ac 100644 --- a/src/dwm-plugins/networks_plugin.cpp +++ b/src/dwm-plugins/networks_plugin.cpp @@ -1003,7 +1003,9 @@ int runNetworksApp(int argc, char** argv) { } std::string cmd = "nmcli connection add type gsm con-name " + shellQuote(cleanApn) + " ifname '*' apn " + - shellQuote(cleanApn) + " connection.autoconnect yes"; + shellQuote(cleanApn) + + " ipv4.method auto ipv6.method ignore" + " connection.autoconnect yes"; if (!trimStr(apnUser).empty()) { cmd += " gsm.username " + shellQuote(trimStr(apnUser)); } @@ -1148,9 +1150,15 @@ int runNetworksApp(int argc, char** argv) { bool turnOn = !cellInfo.gsmActive; setBanner(turnOn ? "Enabling Mobile Data..." : "Disabling Mobile Data..."); render(); + if (turnOn) { + runCaptureCmd("nmcli connection modify " + + shellQuote(cellInfo.gsmConName) + + " ipv4.method auto ipv6.method ignore >/dev/null 2>&1"); + } runCaptureCmd("nmcli connection " + std::string(turnOn ? "up " : "down ") + shellQuote(cellInfo.gsmConName) + " >/dev/null 2>&1"); refreshCurrentTab(false); + runAsyncCmd("sxmo_hook_statusbar.sh modem >/dev/null 2>&1"); } } else if (cellRowSel == 1) { apnName = cellInfo.apn; diff --git a/src/dwm-plugins/ringtones_plugin.cpp b/src/dwm-plugins/ringtones_plugin.cpp index 322db70..2106c9c 100644 --- a/src/dwm-plugins/ringtones_plugin.cpp +++ b/src/dwm-plugins/ringtones_plugin.cpp @@ -46,7 +46,7 @@ struct RingSettings { int ringVol = 100; int ringTimeSec = 20; int ringLoops = 10; - int vibrateStrength = 1; + int vibrateStrength = 2; }; constexpr int kRingTimeSteps[] = {10, 15, 20, 25, 30, 45, 60}; @@ -164,6 +164,10 @@ std::string loadActiveRingtonePath() { } const char* envRing = std::getenv("SXMO_RINGTONE"); if (envRing && *envRing) return envRing; + struct stat stDef {}; + if (::stat("/usr/share/sxmo/ringtones/Banana_Chime.wav", &stDef) == 0) { + return "/usr/share/sxmo/ringtones/Banana_Chime.wav"; + } return "/usr/share/sxmo/ring.ogg"; } @@ -386,15 +390,22 @@ int runRingtonesApp(int argc, char** argv) { settings = loadRingSettings(); int rvol = std::clamp(settings.ringVol, 0, 100); int rx3 = (rvol <= 0) ? 0 : (60 + rvol * 34 / 100); - int mpvVol = rvol * 130 / 100; + int mpvVol = rvol; std::string volArg = "--volume=" + std::to_string(mpvVol); runAsyncCmd("amixer -c 0 cset \"name='EAR_S'\" 0 >/dev/null 2>&1; " + "amixer -c 0 cset \"name='RX1 MIX1 INP1'\" 0 >/dev/null 2>&1; " + "amixer -c 0 cset \"name='PRI_MI2S_RX Audio Mixer MultiMedia1'\" 1 >/dev/null 2>&1; " "amixer -c 0 cset \"name='SPK DAC Switch'\" 1 >/dev/null 2>&1; " "amixer -c 0 cset \"name='RX3 MIX1 INP1'\" 3 >/dev/null 2>&1; " "amixer -c 0 cset \"name='RX3 Digital Volume'\" " + std::to_string(rx3) + " >/dev/null 2>&1; " "wpctl set-mute @DEFAULT_AUDIO_SINK@ 0 >/dev/null 2>&1; " - "wpctl set-volume @DEFAULT_AUDIO_SINK@ 100% >/dev/null 2>&1"); + "wpctl set-volume @DEFAULT_AUDIO_SINK@ " + + std::to_string(rvol) + "% >/dev/null 2>&1; " + "(sleep 0.25; amixer -c 0 cset \"name='RX3 Digital Volume'\" " + + std::to_string(rx3) + " >/dev/null 2>&1; " + "wpctl set-volume @DEFAULT_AUDIO_SINK@ " + + std::to_string(rvol) + "% >/dev/null 2>&1) &"); pid_t pid = ::fork(); if (pid == 0) { std::string runtimeDir = "/run/user/" + std::to_string(::getuid()); @@ -410,7 +421,8 @@ int runRingtonesApp(int argc, char** argv) { ::dup2(devNull, STDERR_FILENO); if (devNull > 2) ::close(devNull); } - ::execlp("mpv", "mpv", "--no-resume-playback", "--quiet", + ::execlp("mpv", "mpv", "--no-config", "--no-terminal", + "--no-resume-playback", "--quiet", "--vo=null", "--no-video", "--ao=pipewire,pulse,alsa", "--audio-channels=stereo", "--audio-samplerate=48000", "--volume-max=150", volArg.c_str(), @@ -564,6 +576,8 @@ int runRingtonesApp(int argc, char** argv) { "Vibrate Only", "Silent", }; + const char* vibLabels[3] = {"1 (Light)", "2 (Medium)", "3 (Strong)"}; + int vIdx = std::clamp(settings.vibrateStrength, 1, 3) - 1; std::string rows[6][2] = { {"Ringer Mode", "< " + std::string(modeLabels[settings.ringerMode % 4]) + " >"}, @@ -574,7 +588,7 @@ int runRingtonesApp(int argc, char** argv) { {"Ring Loop Count", "< " + std::to_string(settings.ringLoops) + " loops >"}, {"Vibrate Strength", - "< Level " + std::to_string(settings.vibrateStrength) + " >"}, + "< Level " + std::string(vibLabels[vIdx]) + " >"}, {"Test Vibration", "Press OK to vibrate"}, }; @@ -665,6 +679,13 @@ int runRingtonesApp(int argc, char** argv) { } else if (settingsRow == 1) { settings.ringVol = std::clamp(settings.ringVol + delta * 5, 0, 100); saveRingSettings(settings); + if (previewPid > 0) { + int rx3 = (settings.ringVol <= 0) ? 0 : (60 + settings.ringVol * 34 / 100); + runAsyncCmd("amixer -c 0 cset \"name='RX3 Digital Volume'\" " + + std::to_string(rx3) + " >/dev/null 2>&1; " + "wpctl set-volume @DEFAULT_AUDIO_SINK@ " + + std::to_string(settings.ringVol) + "% >/dev/null 2>&1"); + } setBanner("Ring volume: " + std::to_string(settings.ringVol) + "%"); } else if (settingsRow == 2) { int curIdx = 2; @@ -692,17 +713,14 @@ int runRingtonesApp(int argc, char** argv) { settings.vibrateStrength = ((settings.vibrateStrength - 1 + 3 + delta) % 3) + 1; saveRingSettings(settings); + runAsyncCmd("SXMO_VIBRATE_FORCE=1 sxmo_vibrate 450 " + + std::to_string(settings.vibrateStrength) + " >/dev/null 2>&1"); + setBanner("Vibration level: " + std::to_string(settings.vibrateStrength)); } else if (settingsRow == 5) { - int mag = (settings.vibrateStrength == 1) - ? 8000 - : (settings.vibrateStrength == 3 ? 65535 : 32000); - runAsyncCmd( - "for d in /dev/input/by-path/platform-vibrator-event " - "/dev/input/by-path/*vibrator*-event; do " - "[ -e \"$d\" ] && export SXMO_VIBRATE_DEV=\"$d\" && break; done; " - "sxmo_vibrate 600 " + - std::to_string(mag) + " >/dev/null 2>&1"); - setBanner("Testing vibration..."); + runAsyncCmd("SXMO_VIBRATE_FORCE=1 sxmo_vibrate 800 " + + std::to_string(settings.vibrateStrength) + " >/dev/null 2>&1"); + setBanner("Testing vibration (Level " + + std::to_string(settings.vibrateStrength) + ")..."); } }; diff --git a/src/dwm-plugins/sxmo_vibrate.c b/src/dwm-plugins/sxmo_vibrate.c index 910a596..8bc5f87 100644 --- a/src/dwm-plugins/sxmo_vibrate.c +++ b/src/dwm-plugins/sxmo_vibrate.c @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -9,17 +10,42 @@ #include #include -static int read_configured_level(void) { - char path[512]; +static volatile sig_atomic_t g_stop = 0; + +static void handle_signal(int sig) { + (void)sig; + g_stop = 1; +} + +static void get_sxmo_config_dir(char* out, size_t out_sz) { const char* cfg = getenv("XDG_CONFIG_HOME"); const char* home = getenv("HOME"); if (cfg && *cfg) { - snprintf(path, sizeof(path), "%s/sxmo/ringsettings", cfg); + snprintf(out, out_sz, "%s/sxmo", cfg); } else if (home && *home) { - snprintf(path, sizeof(path), "%s/.config/sxmo/ringsettings", home); + snprintf(out, out_sz, "%s/.config/sxmo", home); } else { - snprintf(path, sizeof(path), "/home/user/.config/sxmo/ringsettings"); + snprintf(out, out_sz, "/home/user/.config/sxmo"); } +} + +static int is_vibrate_muted(void) { + const char* force = getenv("SXMO_VIBRATE_FORCE"); + if (force && *force == '1') { + return 0; + } + char dir[448]; + char path[512]; + get_sxmo_config_dir(dir, sizeof(dir)); + snprintf(path, sizeof(path), "%s/.novibrate", dir); + return access(path, F_OK) == 0; +} + +static int read_configured_level(void) { + char dir[448]; + char path[512]; + get_sxmo_config_dir(dir, sizeof(dir)); + snprintf(path, sizeof(path), "%s/ringsettings", dir); FILE* fp = fopen(path, "r"); if (!fp) return 0; @@ -32,24 +58,45 @@ static int read_configured_level(void) { if (strncmp(p, "SXMO_VIBRATE_STRENGTH=", 22) == 0) { int v = atoi(p + 22); if (v >= 1 && v <= 3) level = v; + else if (v > 3 && v <= 16000) level = 1; + else if (v > 16000 && v <= 45000) level = 2; + else if (v > 45000) level = 3; } } fclose(fp); return level; } -static int resolve_magnitude(int raw_arg, int has_arg) { +static int resolve_level_and_magnitude(int raw_arg, int has_arg, int* level_out) { int val = raw_arg; - if (!has_arg || (val == 1 && !getenv("SXMO_VIBRATE_STRENGTH"))) { + const char* env_str = getenv("SXMO_VIBRATE_STRENGTH"); + if (!has_arg || (val == 1 && (!env_str || !*env_str))) { int cfg = read_configured_level(); - if (cfg >= 1 && cfg <= 3) val = cfg; + if (cfg >= 1 && cfg <= 3) { + val = cfg; + } else if (env_str && *env_str) { + int ev = atoi(env_str); + if (ev >= 1) val = ev; + } else if (!has_arg) { + val = 2; + } } - if (val <= 1) return 8000; - if (val == 2) return 32000; - if (val == 3) return 65535; - if (val < 256) return val * 257; - if (val > 65535) return 65535; - return val; + + int level = 2; + int mag = 32000; + if (val <= 1 || (val >= 256 && val <= 16000)) { + level = 1; + mag = 14000; + } else if (val == 2 || (val > 16000 && val <= 45000)) { + level = 2; + mag = 32000; + } else { + level = 3; + mag = 65535; + } + + if (level_out) *level_out = level; + return mag; } static int open_vibrator_device(void) { @@ -77,6 +124,15 @@ static int open_vibrator_device(void) { return -1; } +static void sleep_ms_interruptible(long ms) { + if (ms <= 0 || g_stop) return; + struct timespec req; + req.tv_sec = ms / 1000; + req.tv_nsec = (ms % 1000) * 1000000L; + while (!g_stop && nanosleep(&req, &req) == -1 && errno == EINTR) { + } +} + int main(int argc, char** argv) { if (argc < 2 || argc > 3) { fprintf(stderr, "Usage: sxmo_vibrate duration_ms [strength_number]\n"); @@ -91,6 +147,10 @@ int main(int argc, char** argv) { return 1; } + if (is_vibrate_muted()) { + return 0; + } + int raw_strength = 2; int has_strength = 0; if (argc == 3) { @@ -103,7 +163,16 @@ int main(int argc, char** argv) { } } - int magnitude = resolve_magnitude(raw_strength, has_strength); + int level = 2; + int magnitude = resolve_level_and_magnitude(raw_strength, has_strength, &level); + + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = handle_signal; + sigemptyset(&sa.sa_mask); + sigaction(SIGINT, &sa, NULL); + sigaction(SIGTERM, &sa, NULL); + sigaction(SIGHUP, &sa, NULL); int fd = open_vibrator_device(); if (fd < 0) { @@ -128,18 +197,30 @@ int main(int argc, char** argv) { memset(&play, 0, sizeof(play)); play.type = EV_FF; play.code = (unsigned short)effect.id; - play.value = 3; - if (write(fd, &play, sizeof(play)) < 0) { - ioctl(fd, EVIOCRMFF, effect.id); - close(fd); - return 1; - } - - struct timespec req; - req.tv_sec = duration_ms / 1000; - req.tv_nsec = (duration_ms % 1000) * 1000000L; - while (nanosleep(&req, &req) == -1 && errno == EINTR) { + if (level >= 3 || duration_ms <= 90) { + long actual_ms = (level == 1 && duration_ms > 55) ? 55 : duration_ms; + play.value = 1; + if (write(fd, &play, sizeof(play)) >= 0) { + sleep_ms_interruptible(actual_ms); + } + } else { + long on_ms = (level == 1) ? 75 : 160; + long off_ms = (level == 1) ? 125 : 80; + long rem = duration_ms; + while (rem > 0 && !g_stop) { + long cur_on = (rem < on_ms) ? rem : on_ms; + play.value = 1; + if (write(fd, &play, sizeof(play)) < 0) break; + sleep_ms_interruptible(cur_on); + rem -= cur_on; + play.value = 0; + (void)write(fd, &play, sizeof(play)); + if (rem <= 0 || g_stop) break; + long cur_off = (rem < off_ms) ? rem : off_ms; + sleep_ms_interruptible(cur_off); + rem -= cur_off; + } } play.value = 0; diff --git a/src/xdrawer/xdrawer.cpp b/src/xdrawer/xdrawer.cpp index daa80d6..42a0332 100644 --- a/src/xdrawer/xdrawer.cpp +++ b/src/xdrawer/xdrawer.cpp @@ -43,7 +43,7 @@ constexpr int kWinX = 0; constexpr int kWinY = kTopBarHeight; constexpr int kWinWidth = kScreenWidth; constexpr int kWinHeight = kScreenHeight - kTopBarHeight; // 296px flush below 24px dwm bar -constexpr int kRowCount = 9; +constexpr int kRowCount = 10; // Nerd Font PUA icons matching sxmo_hook_icons.sh & dwm ("Fira Code, DejaVuSansM Nerd Font") constexpr const char* kIconSpeaker = "\xef\x80\xa8"; // U+F028 speaker @@ -242,11 +242,12 @@ enum class ControlId { EarpieceVol = 1, RingVol = 2, RingerMode = 3, - Flashlight = 4, - SoundMute = 5, - MicMute = 6, - ScreenBright = 7, - KbdBright = 8, + VibrateStrength = 4, + Flashlight = 5, + SoundMute = 6, + MicMute = 7, + ScreenBright = 8, + KbdBright = 9, }; struct ControlItem { @@ -300,15 +301,15 @@ class PhoneController { items_.push_back({ControlId::SpeakerVol, kIconSpeaker, kIconSoundOff, "Speaker", true, 0, 100, 5, spkPct}); - // 2. Earpiece volume (0..100%, mapped to ALSA RX1 Digital Volume 58..90) - long rx1 = alsa_.getInteger("RX1 Digital Volume", 84); - int earPct = 100; + // 2. Earpiece volume (0..100%, mapped to ALSA RX1 Digital Volume 68..92) + long rx1 = alsa_.getInteger("RX1 Digital Volume", 88); + int earPct = 85; if (rx1 <= 0) { earPct = 0; - } else if (rx1 < 84) { - earPct = std::clamp(static_cast(std::lround((rx1 * 100.0) / 84.0)), 5, 100); + } else if (rx1 >= 68) { + earPct = std::clamp(static_cast(std::lround(((rx1 - 68) * 100.0) / 24.0)), 5, 100); } - earPct = (earPct / 5) * 5; + earPct = ((earPct + 2) / 5) * 5; items_.push_back({ControlId::EarpieceVol, kIconEarpiece, kIconEarpiece, "Earpiece", true, 0, 100, 5, earPct}); @@ -322,6 +323,11 @@ class PhoneController { items_.push_back({ControlId::RingerMode, kIconBell, kIconBellSlash, "Ringer", false, 0, 3, 1, ringerVal}); + // 5. Vibration Strength (1=Light, 2=Medium, 3=Strong) + int vibVal = readVibrateStrength(); + items_.push_back({ControlId::VibrateStrength, kIconBell, kIconBell, + "Vibration", false, 1, 3, 1, vibVal}); + // 5. Flashlight on/off (toggle) int torchCur = readIntFile(torchDir_ + "/brightness", 0); items_.push_back({ControlId::Flashlight, kIconFlashlight, kIconFlashlight, @@ -373,6 +379,8 @@ class PhoneController { it.value = std::clamp(it.value + dir * it.stepVal, it.minVal, it.maxVal); } else if (it.id == ControlId::RingerMode) { it.value = (it.value + dir + 4) % 4; + } else if (it.id == ControlId::VibrateStrength) { + it.value = ((it.value - 1 + dir + 3) % 3) + 1; } else { it.value = it.value ? 0 : 1; } @@ -387,6 +395,9 @@ class PhoneController { if (it.id == ControlId::RingerMode) { it.value = (it.value + 1) % 4; apply(it); + } else if (it.id == ControlId::VibrateStrength) { + it.value = (it.value % 3) + 1; + apply(it); } else if (!it.isSlider) { it.value = it.value ? 0 : 1; apply(it); @@ -432,13 +443,30 @@ class PhoneController { return 100; } - static void writeRingVolume(int ringVol) { + static int readVibrateStrength() { + std::ifstream ifs(getSxmoCfgDir() + "/ringsettings"); + std::string line; + while (std::getline(ifs, line)) { + auto eq = line.find('='); + if (eq == std::string::npos) continue; + std::string k = line.substr(0, eq); + std::string v = line.substr(eq + 1); + if (k == "SXMO_VIBRATE_STRENGTH") { + int val = std::atoi(v.c_str()); + if (val >= 1 && val <= 3) return val; + } + } + return 2; + } + + static void writeRingAndVibSettings(int newRingVol, int newVibStrength) { std::string cfg = getSxmoCfgDir(); ::mkdir(cfg.c_str(), 0755); std::string path = cfg + "/ringsettings"; int ringTime = 20; int ringLoops = 10; int vibStrength = 2; + int ringVol = 100; { std::ifstream ifs(path); std::string line; @@ -450,14 +478,17 @@ class PhoneController { if (k == "SXMO_RINGTIME" && val >= 5 && val <= 120) ringTime = val; else if (k == "SXMO_RINGNUMBER" && val >= 1 && val <= 100) ringLoops = val; else if (k == "SXMO_VIBRATE_STRENGTH" && val >= 1 && val <= 3) vibStrength = val; + else if (k == "SXMO_RINGVOL" && val >= 0 && val <= 100) ringVol = val; } } + if (newRingVol >= 0) ringVol = std::clamp(newRingVol, 0, 100); + if (newVibStrength >= 1 && newVibStrength <= 3) vibStrength = newVibStrength; std::ofstream ofs(path, std::ios::trunc); if (ofs) { ofs << "SXMO_RINGTIME=" << ringTime << "\n"; ofs << "SXMO_RINGNUMBER=" << ringLoops << "\n"; ofs << "SXMO_VIBRATE_STRENGTH=" << vibStrength << "\n"; - ofs << "SXMO_RINGVOL=" << std::clamp(ringVol, 0, 100) << "\n"; + ofs << "SXMO_RINGVOL=" << ringVol << "\n"; } } @@ -484,12 +515,21 @@ class PhoneController { break; } case ControlId::EarpieceVol: { - long hw = (it.value <= 0) ? 0 : (58 + std::lround((it.value * 32.0) / 100.0)); + long hw = (it.value <= 0) ? 0 : (68 + std::lround((it.value * 24.0) / 100.0)); alsa_.setInteger("RX1 Digital Volume", hw); break; } case ControlId::RingVol: { - writeRingVolume(it.value); + writeRingAndVibSettings(it.value, -1); + break; + } + case ControlId::VibrateStrength: { + writeRingAndVibSettings(-1, it.value); + char cmd[96]; + std::snprintf(cmd, sizeof(cmd), + "SXMO_VIBRATE_FORCE=1 sxmo_vibrate 400 %d >/dev/null 2>&1", + it.value); + runAsyncCmd(cmd); break; } case ControlId::Flashlight: { @@ -792,6 +832,13 @@ static int xdrawer_run_main(int argc, char** argv) { XftColor* rCol = (it.value == 0) ? (isSel ? &colYellow : &colGreen) : ((it.value == 1) ? &colYellow : &colMuted); drawPango(layoutSmall, rCol, kWinWidth - stW - 6, ry, rowH, stStr); + } else if (it.id == ControlId::VibrateStrength) { + const char* vModes[3] = {"Light", "Medium", "Strong"}; + int vIdx = std::clamp(it.value, 1, 3) - 1; + std::string stStr = std::string("< ") + vModes[vIdx] + " >"; + int stW = pangoWidth(layoutSmall, stStr); + XftColor* vCol = isSel ? &colYellow : &colGreen; + drawPango(layoutSmall, vCol, kWinWidth - stW - 6, ry, rowH, stStr); } else { bool on = (it.value != 0); std::string stStr = on ? "ON" : "OFF";