1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
|
#!/usr/bin/env bash
# CRDOP launcher — native MAX25-SoftModem (audio-dummyd / M25 host).
# ARDOP is a separate optional MAX25-Stack plugin — not part of CRDOP.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
MAX25_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
PREFIX_SHARE="${SCRIPT_DIR}/../share/crdop"
if [[ -n "${CRDOP_INI:-}" ]]; then
INI="${CRDOP_INI}"
elif [[ -f "${HOME}/.config/crdop/crdop.ini" ]]; then
INI="${HOME}/.config/crdop/crdop.ini"
elif [[ -f "${PREFIX_SHARE}/crdop.ini.example" ]]; then
INI="${PREFIX_SHARE}/crdop.ini.example"
else
INI="${ROOT}/share/crdop.ini.example"
fi
ini_get() {
local section="$1" key="$2" default="$3" file="$4"
[[ -f "${file}" ]] || { echo "${default}"; return; }
awk -F= -v sec="[${section}]" -v k="${key}" -v d="${default}" '
$0 ~ /^[[:space:]]*[#;]/ { next }
$1 ~ /^\[/ { cur=$0; gsub(/[][]/, "", cur); next }
cur == substr(sec, 2, length(sec)-2) {
gsub(/^[[:space:]]+|[[:space:]]+$/, "", $1)
if ($1 == k) {
v=$2; gsub(/^[[:space:]]+|[[:space:]]+$/, "", v); gsub(/^"|"$/, "", v)
print v; found=1; exit
}
}
END { if (!found) print d }
' "${file}"
}
resolve_ini() {
if [[ ! -f "${INI}" && -f "${ROOT}/share/crdop.ini.example" ]]; then
INI="${ROOT}/share/crdop.ini.example"
fi
}
normalize_profile() {
case "${1,,}" in
cb|dual|amateur) echo "${1,,}" ;;
*)
echo "warn: unknown radio_profile '${1}', using cb" >&2
echo "cb"
;;
esac
}
normalize_duplex() {
case "${1,,}" in
full|half) echo "${1,,}" ;;
*)
echo "warn: unknown duplex '${1}', using half" >&2
echo "half"
;;
esac
}
enforce_host_audio() {
local backend
backend="$(ini_get audio backend "" "${INI}")"
if [[ -z "${backend}" ]]; then
case "$(uname -s)" in
FreeBSD) backend="oss" ;;
*) backend="alsa" ;;
esac
fi
if [[ "${backend}" == "alsa" ]]; then
local no_pulse
no_pulse="$(ini_get audio no_pulse yes "${INI}")"
case "${no_pulse,,}" in
1|yes|true|on)
unset PULSE_SERVER PULSE_COOKIE PIPEWIRE_RUNTIME_DIR 2>/dev/null || true
export PULSE_SERVER=
export ALSA_CARD="${ALSA_CARD:-}"
;;
esac
fi
}
audio_cli_args() {
local cap pb backend
backend="$(ini_get audio backend "" "${INI}")"
if [[ -z "${backend}" ]]; then
case "$(uname -s)" in
FreeBSD) backend="oss" ;;
*) backend="alsa" ;;
esac
fi
cap="$(ini_get audio capture "" "${INI}")"
pb="$(ini_get audio playback "" "${INI}")"
local args=()
[[ -n "${cap}" ]] && args+=("${cap}")
[[ -n "${pb}" ]] && args+=("${pb}")
if [[ ${#args[@]} -gt 0 ]]; then
if [[ "${backend}" == "alsa" ]]; then
for a in "${args[@]}"; do
if [[ "${a}" == *pulse* || "${a}" == *pipewire* || "${a}" == default ]]; then
echo "error: audio device '${a}' — use hw:/plughw: kernel ALSA" >&2
exit 1
fi
done
fi
printf '%s\n' "${args[@]}"
elif [[ "${backend}" == "oss" ]]; then
echo "/dev/dsp"
echo "/dev/dsp"
fi
}
find_audio_dummyd() {
for candidate in \
"${MAX25_ROOT}/build/bin/audio-dummyd" \
"${MAX25_ROOT}/stacks/crdop/tools/audio-dummyd" \
"$(command -v audio-dummyd 2>/dev/null || true)"; do
[[ -n "${candidate}" && -x "${candidate}" ]] || continue
echo "${candidate}"
return
done
return 1
}
resolve_ini
if [[ -f "${PREFIX_SHARE}/VERSION" ]]; then
CRDOP_VERSION="$(tr -d '[:space:]' < "${PREFIX_SHARE}/VERSION")"
elif [[ -f "${ROOT}/VERSION" ]]; then
CRDOP_VERSION="$(tr -d '[:space:]' < "${ROOT}/VERSION")"
else
CRDOP_VERSION="CUR999"
fi
PROFILE="$(normalize_profile "$(ini_get profile radio_profile cb "${INI}")")"
DUPLEX="$(normalize_duplex "$(ini_get modem duplex half "${INI}")")"
MYCALL="$(ini_get mycall call NOCALL-0 "${INI}")"
HOST_PORT="$(ini_get host port 8515 "${INI}")"
DATA_PORT=$((HOST_PORT + 1))
SAMPLE_RATE="$(ini_get audio sample_rate 48000 "${INI}")"
if [[ $# -ge 1 && "${1}" =~ ^[0-9]+$ ]]; then
HOST_PORT="$1"
DATA_PORT=$((HOST_PORT + 1))
shift
fi
mapfile -t AUDIO_ARGS < <(audio_cli_args || true)
enforce_host_audio
DUMMY="$(find_audio_dummyd || true)"
if [[ -z "${DUMMY}" ]]; then
echo "error: audio-dummyd not found — build MAX25 or set path" >&2
exit 1
fi
DUMMY_ARGS=(--ctrl-port "${HOST_PORT}" --data-port "${DATA_PORT}" -r "${SAMPLE_RATE}")
if [[ ${#AUDIO_ARGS[@]} -ge 1 ]]; then
DUMMY_ARGS+=(-D "${AUDIO_ARGS[0]}")
fi
if [[ ${#AUDIO_ARGS[@]} -ge 2 ]]; then
DUMMY_ARGS+=(-P "${AUDIO_ARGS[1]}")
fi
AUDIO_BACKEND="$(ini_get audio backend "" "${INI}")"
[[ -z "${AUDIO_BACKEND}" ]] && case "$(uname -s)" in FreeBSD) AUDIO_BACKEND="oss" ;; *) AUDIO_BACKEND="alsa" ;; esac
echo "CRDOP ${CRDOP_VERSION} profile=${PROFILE} duplex=${DUPLEX} ctrl=:${HOST_PORT} data=:${DATA_PORT} call=${MYCALL} audio=${AUDIO_BACKEND} (native M25)"
exec "${DUMMY}" "${DUMMY_ARGS[@]}" "$@"
|