diff options
author | David Timber <dxdt@dev.snart.me> | 2025-06-05 23:18:47 +0900 |
---|---|---|
committer | David Timber <dxdt@dev.snart.me> | 2025-06-05 23:18:47 +0900 |
commit | 3f732926e7a43baba583d55ac575594e339247ae (patch) | |
tree | 7d8eba9c630c0b3e90065757c4e30dde82d57b51 | |
parent | 35f2ceaa417262bab002067e65b6a84389a199e0 (diff) |
mmfwd-callam: add local playback support
-rw-r--r-- | README.md | 44 | ||||
-rw-r--r-- | doc/mmfwd.sample.yaml | 8 | ||||
-rw-r--r-- | src/mmfwd-callam.cpp | 154 | ||||
-rw-r--r-- | src/mmfwd/__init__.py | 18 |
4 files changed, 199 insertions, 25 deletions
@@ -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, |