aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Timber <dxdt@dev.snart.me>2025-06-05 23:18:47 +0900
committerDavid Timber <dxdt@dev.snart.me>2025-06-05 23:18:47 +0900
commit3f732926e7a43baba583d55ac575594e339247ae (patch)
tree7d8eba9c630c0b3e90065757c4e30dde82d57b51
parent35f2ceaa417262bab002067e65b6a84389a199e0 (diff)
mmfwd-callam: add local playback support
-rw-r--r--README.md44
-rw-r--r--doc/mmfwd.sample.yaml8
-rw-r--r--src/mmfwd-callam.cpp154
-rw-r--r--src/mmfwd/__init__.py18
4 files changed, 199 insertions, 25 deletions
diff --git a/README.md b/README.md
index c29fbe2..f84e237 100644
--- a/README.md
+++ b/README.md
@@ -95,6 +95,50 @@ To encode the recorded raw audio files:
ffmpeg -ar 16000 -ac 1 -f s16le -i PCM_FILE -c:a libopus OUTFILE.ogg
```
+## Local playback
+Config to enable. `1` for incoming audio only, `2` for outgoing audio only, `3`
+for both. `0` to disable:
+
+```yaml
+mmfwd:
+ instances:
+ -
+...
+ call-am:
+...
+ playback: 1
+...
+```
+
+### Enable ringtone (optional)
+Make a simple ringtone:
+
+```sh
+# short bursts of alternating 250 and 500 Hz sine waves
+truncate -s 0 bell.pcm
+for (( i = 0; i < 6; i += 1 ))
+do
+ ffmpeg -loglevel error -f lavfi -i "sine=frequency=250:duration=0.1" -f s16le -ar 16000 -ac 1 - >> bell.pcm
+ ffmpeg -loglevel error -f lavfi -i "sine=frequency=500:duration=0.2" -f s16le -ar 16000 -ac 1 - >> bell.pcm
+done
+
+# encode it to opus
+ffmpeg -loglevel error -f s16le -ar 16000 -ac 1 -i bell.pcm -c:a libopus ringtone.ogg
+```
+
+Config to enable ringtone:
+
+```yaml
+ ringtone-exec:
+ - ffplay
+ - -loglevel
+ - error
+ - -autoexit
+ - -nodisp
+ - ringtone.ogg
+...
+```
+
## MM Patches
- https://gitlab.freedesktop.org/mobile-broadband/ModemManager/-/merge_requests/1293
- https://gitlab.freedesktop.org/mobile-broadband/ModemManager/-/issues/996
diff --git a/doc/mmfwd.sample.yaml b/doc/mmfwd.sample.yaml
index 0d4396a..f0e9d91 100644
--- a/doc/mmfwd.sample.yaml
+++ b/doc/mmfwd.sample.yaml
@@ -12,3 +12,11 @@ mmfwd:
call-am:
enabled: false
exec: "/usr/local/mmfwd-callam"
+ playback: 0
+ ringtone-exec:
+ - ffplay
+ - -loglevel
+ - error
+ - -autoexit
+ - -nodisp
+ - ringtone.ogg
diff --git a/src/mmfwd-callam.cpp b/src/mmfwd-callam.cpp
index 90ad4d2..3104dd7 100644
--- a/src/mmfwd-callam.cpp
+++ b/src/mmfwd-callam.cpp
@@ -31,10 +31,7 @@ struct {
const char *hello_sample_pat; // glob patterns
const char *outfile_prefix;
struct {
- const char *sample_rate;
- const char *channels;
- const char *fmt;
- bool playback; // TODO
+ bool enabled[2];
} playback;
int vl;
} params;
@@ -66,9 +63,9 @@ size_t getrndsize (void) {
void init_params (void) {
params.hello_sample_pat = "hello/*.pcm";
- params.playback.sample_rate = "8000";
- params.playback.channels = "1";
- params.playback.fmt = "S16_LE";
+ // params.playback.sample_rate = "8000";
+ // params.playback.channels = "1";
+ // params.playback.fmt = "S16_LE";
}
void init_g (void) {
@@ -77,6 +74,108 @@ void init_g (void) {
g.playback[0] = g.playback[1] = -1;
}
+void parse_env (void) {
+ const char *env;
+
+ env = getenv("MMFWD_CALLAM_PLAYBACK");
+ if (env != NULL) {
+ int v = 0;
+
+ sscanf(env, "%d", &v);
+ params.playback.enabled[0] = v & 1 ? true : false;
+ params.playback.enabled[1] = v & 2 ? true : false;
+ }
+}
+
+pid_t do_playback_exec (const int p_stdin, const int p_stdout) {
+ pid_t pid;
+ int fr;
+
+ pid = fork();
+ if (pid < 0) {
+ return -1;
+ }
+ else if (pid > 0) {
+ return pid;
+ }
+
+ close(STDIN_FILENO);
+ close(STDOUT_FILENO);
+ dup2(p_stdin, STDIN_FILENO);
+ dup2(p_stdout, STDOUT_FILENO);
+ close(p_stdin);
+ close(p_stdout);
+
+ fr = system(
+ "ffmpeg -loglevel error -f s16le -ar 16000 -ac 1 -i pipe: -filter:a loudnorm -ar 16000 -ac 1 -f s16le - | "
+ "aplay -q -r 16000 -c 1 -f S16_LE -"
+ );
+ if (fr < 0) {
+ perror(ARGV0 ": system()");
+ abort();
+ }
+ exit(fr);
+
+ assert("unreachable" && false);
+}
+
+bool open_playback (void) {
+ int p[2];
+ int fr;
+ pid_t pid[2] = { -1, -1 };
+ int fd_in[2] = { -1, -1 };
+ int fd_out[2] = { -1, -1 };
+ int blackhole;
+
+ fr = pipe(p);
+ if (fr < 0) {
+ perror(ARGV0 ": pipe()");
+ goto ERR;
+ }
+ close(p[0]);
+ blackhole = p[1];
+
+ for (size_t i = 0; i < 2; i += 1) {
+ if (!params.playback.enabled[i]) {
+ continue;
+ }
+
+ fr = pipe(p);
+ if (fr < 0) {
+ perror(ARGV0 ": pipe()");
+ goto ERR;
+ }
+ fd_in[i] = p[0];
+ fd_out[i] = p[1];
+
+ pid[i] = do_playback_exec(p[0], blackhole);
+ if (pid[i] < 0) {
+ goto ERR;
+ }
+
+ g.playback[i] = p[1];
+ close(fd_in[i]);
+ fd_in[i] = -1;
+
+ // Let audio skip when the buffer gets full.
+ // Should be fine as long as the size of the buffer is aligned to 2 byte
+ // boundary which is always the case.
+ fr = fcntl(g.playback[i], F_GETFL);
+ fr |= O_NONBLOCK;
+ fr = fcntl(g.playback[i], F_SETFL);
+ assert(fr == 0);
+ }
+
+ return true;
+ERR: // house-keeping
+ for (size_t i = 0; i < 2; i += 1) {
+ close(fd_in[i]);
+ close(fd_out[i]);
+ kill(pid[i], SIGHUP);
+ }
+ return false;
+}
+
bool open_dev (void) {
struct termios tis;
const int fd = open(params.dev, O_RDWR | O_NONBLOCK);
@@ -209,6 +308,23 @@ bool write_full (const int fd, const void *m, size_t len, const std::string &fn)
return true;
}
+void sink_playback_audio (int *fd, const void *buf, const size_t len) {
+ ssize_t iofr;
+
+ if (*fd < 0) {
+ return;
+ }
+
+ iofr = write(*fd, buf, len);
+ // Errors are not fatal:
+ // - skip some samples when the child process struggle/stopped
+ // - EPIPE in case of death of child process
+ if (iofr < 0 && errno == EPIPE) {
+ close(*fd);
+ *fd = -1;
+ }
+}
+
bool loop_main (void) {
uint8_t buf[4096];
size_t sample_pos = 0;
@@ -259,14 +375,7 @@ bool loop_main (void) {
if (!write_full(g.fd[0], buf, blen, g.outfile[0])) {
return false;
}
-
- if (g.playback[0] >= 0) {
- iofr = write(g.playback[0], buf, blen);
- if (iofr < 0) {
- close(g.playback[0]);
- g.playback[0] = -1;
- }
- }
+ sink_playback_audio(&g.playback[0], buf, blen);
}
if (pfd.events & POLLOUT) {
@@ -294,14 +403,7 @@ bool loop_main (void) {
if (!write_full(g.fd[1], ptr, blen, g.outfile[1])) {
return false;
}
-
- if (g.playback[1] >= 0) {
- iofr = write(g.playback[1], ptr, blen);
- if (iofr < 0) {
- close(g.playback[1]);
- g.playback[1] = -1;
- }
- }
+ sink_playback_audio(&g.playback[1], buf, blen);
sample_pos += blen;
if (sample_pos >= g.hsample.len) {
@@ -328,6 +430,8 @@ void handle_termsig (int) {
int main (const int argc, const char **argv) {
init_params();
init_g();
+
+ parse_env();
// TODO: getopt()
if (argc <= 2) {
@@ -340,7 +444,9 @@ int main (const int argc, const char **argv) {
params.dev = argv[1];
params.outfile_prefix = argv[2];
- if (!open_dev() || !open_hsample() || !open_fds()) {
+ signal(SIGPIPE, SIG_IGN);
+
+ if (!open_playback() || !open_dev() || !open_hsample() || !open_fds()) {
return 1;
}
diff --git a/src/mmfwd/__init__.py b/src/mmfwd/__init__.py
index 17d1485..2c34d99 100644
--- a/src/mmfwd/__init__.py
+++ b/src/mmfwd/__init__.py
@@ -70,6 +70,7 @@ class Instance:
'enabled': False
})
self.callam_proc = None
+ self.callam_ringtone_proc = None
self.callam_timer = None
def match (self, m) -> bool:
@@ -325,6 +326,11 @@ class Application:
else:
call.hangup(None, self.on_call_hangup, ud)
+ if ud.instance.callam_ringtone_proc is not None:
+ ud.instance.callam_ringtone_proc.terminate()
+ ud.instance.callam_ringtone_proc.wait()
+ ud.instance.callam_ringtone_proc = None
+
if ud.instance.callam_proc is not None:
ud.instance.callam_proc.terminate()
ud.instance.callam_proc.wait()
@@ -359,6 +365,13 @@ class Application:
print("on_call_accept()") # FIXME
call.connect('state-changed', self.on_call_change, ud)
+ if ud.instance.callam.get('ringtone-exec'):
+ try:
+ ud.instance.callam_ringtone_proc = subprocess.Popen(
+ ud.instance.callam['ringtone-exec'])
+ except Exception as e:
+ sys.stderr.write(e + os.linesep)
+
# The custom ModemManager will send AT+CPCMREG.
# mmfwd-callam process will set up the serial, play the hello message
# and record
@@ -373,8 +386,11 @@ class Application:
os.makedirs(dir, exist_ok = True)
+ env = os.environ.copy()
+ env['MMFWD_CALLAM_PLAYBACK'] = str(ud.instance.callam['playback'])
+
exec = [ ud.instance.callam['exec'], "/dev/" + ud.audio_port, path ]
- ud.instance.callam_proc = subprocess.Popen(exec)
+ ud.instance.callam_proc = subprocess.Popen(exec, env = env)
# 5 minutes timeout
ud.instance.callam_timer = GLib.timeout_add_seconds(
60 * 4,