Controlling my stream audio from Emacs: background music, typing sounds, and push to talk

Posted: - Modified: | emacs

Update: 2021-02-11: Parsed pacmd list-sources so that I can mute/unmute devices by regular expression. Update: 2021-02-07: Made it work with my USB microphone.

I was experimenting with streaming Emacs geeking around on twitch.tv. Someone asked me to have soft background music and typing sounds. Since I’m a little clueless about music and don’t want to bother with hunting down nice royalty-free music, I figured I could just use the Mozart dice game to programmatically generate music.

I installed the mozart-dice-game NPM package and used this bit of Javascript to generate a hundred MIDI files.

const x = require('mozart-dice-game')
for (let i = 0; i < 100; i++) { x.saveMinuet('minuet' + String(i).padStart('3', '0') + '.mid'); }

Then I wrote this Emacs Lisp function to turn it on and off.

(defvar my/background-music-process nil "Process for playing background music")
(defun my/stream-toggle-background-music (&optional enable)
  (interactive)
  (if (or my/background-music-process
          (and (numberp enable) (< enable 0)))
      (progn
        (when (process-live-p my/background-music-process)
          (kill-process my/background-music-process))
        (setq my/background-music-process nil))
    (let ((files (directory-files "~/code/music" t "mid\\'")))
      (setq my/background-music-process
            (apply
             'start-process
             "*Music*"
             nil
             (append (list "timidity" "-idlr" "--volume=10") files))))))

People also suggested typing sounds. I guess that’s a good way to get a sense of activity. The default selectric sound was a little too loud for me, so we’ll use the move sound for now. It would be nice to make this more random-sounding someday.

(defun my/selectric-type-sound ()
  "Make the sound of typing."
  ;; Someday, randomize this or something
  (selectric-make-sound (expand-file-name "selectric-move.wav" selectric-files-path)))

(use-package selectric-mode
  :diminish ""
  :config
  (fset #'selectric-type-sound #'my/selectric-type-sound))

I was having a hard time remembering to go back on mute during meetings, since the LED on the mute button wasn’t working at the time and the system tray icon was a little hard to notice. The LED has mysteriously decided to start working again, but push-to-talk is handy anyway. I want to be able to tap a key to toggle my microphone on and off, and hold it down in order to make it push-to-talk. It looks like my key repeat is less than 0.5 seconds, so I can set a timer that will turn things off after a little while. This code doesn’t pick up any changes that happen outside Emacs, but it’ll do for now. I used pacmd list-sources to list the sources and get the IDs.

(defun my/pacmd-set-device (regexp status)
  (with-current-buffer (get-buffer-create "*pacmd*")
    (erase-buffer)
    (shell-command "pacmd list-sources" (current-buffer))
    (goto-char (point-max))
    (let (results)
      (while (re-search-backward regexp nil t)
        (when (re-search-backward "index: \\([[:digit:]]+\\)" nil t)
          (setq results (cons (match-string 1) results))
          (shell-command-to-string (format "pacmd set-source-mute %s %d"
                                           (match-string 1)
                                           (if (equal status 'on) 0 1)))))
      results)))

(defvar my/mic-p nil "Non-nil means microphone is on")
(add-to-list 'mode-line-front-space '(:eval (if my/mic-p "*MIC*" "")))

(defun my/mic-off ()
  (interactive)
  (my/pacmd-set-device "Yeti" 'off)
  (my/pacmd-set-device "Internal Microphone" 'off)
  (setq my/mic-p nil))
(defun my/mic-on ()
  (interactive)
  (my/pacmd-set-device "Yeti" 'on)
  (my/pacmd-set-device "Internal Microphone" 'on)
  (setq my/mic-p t))
(defun my/mic-toggle ()
  (interactive)
  (if my/mic-p (my/mic-off) (my/mic-on)))

(defvar my/push-to-talk-mute-timer nil "Timer to mute things again.")
(defvar my/push-to-talk-last-time nil "Last time my/push-to-talk was run")
(defvar my/push-to-talk-threshold 0.5 "Number of seconds")

(defun my/push-to-talk-mute ()
  (interactive)
  (message "Muting.")
  (my/mic-off)
  (force-mode-line-update)
  (my/obs-websocket-add-subtitle (my/obs-websocket-stream-time-msecs) "[Microphone off]"))

(defun my/push-to-talk ()
  "Tap to toggle microphone on and off, or repeat the command to make it push to talk."
  (interactive)
  (cond
   ((null my/mic-p) ;; It's off, so turn it on
    (when (timerp my/push-to-talk-mute-timer)
      (cancel-timer my/push-to-talk-mute-timer))
    (my/mic-on)
    (my/obs-websocket-add-subtitle (my/obs-websocket-stream-time-msecs) "[Microphone on]")
    (setq my/push-to-talk-last-time (current-time)))
   ((timerp my/push-to-talk-mute-timer) ;; Push-to-talk mode
    (cancel-timer my/push-to-talk-mute-timer)
    (setq my/push-to-talk-mute-timer
          (run-at-time my/push-to-talk-threshold nil #'my/push-to-talk-mute)))
   ;; Might be push to talk, if we're within the key repeating time
   ((< (- (time-to-seconds (current-time)) (time-to-seconds my/push-to-talk-last-time)) 
       my/push-to-talk-threshold)
    (setq my/push-to-talk-mute-timer
          (run-at-time my/push-to-talk-threshold nil #'my/push-to-talk-mute)))
   ;; It's been a while since I turned the mic on.
   (t (my/push-to-talk-mute))))

(global-set-key (kbd "<f12>") #'my/push-to-talk)
You can comment with Disqus or you can e-mail me at sacha@sachachua.com.