This is a technical article on how v0.0.1 of hella-sounds.el works.
Approach
The hella-sound.el experiment rests upon a core mechanic in emacs: advice
; specifically nadvice.el
. This design decision can be overcome if needed by creating combinators myself, but it is what it is right now.
In rough bullet points:
- I defined a datastructure / format for people to map sounds to functions.
'(:sound-mappings ((:sound "closed-triangle-short.ogg.snd" :fns ((self-insert-command) (org-self-insert-command))) (:sound "harp-note-d2.aiff" :fns ((forward-sentence)))))
- Processing this data structure occurs where I hook up advice to various functions that users want events for.
- I advice the function
forward-sentence
, with a dynamically generated function that will playharp-note-d2.aiff
. In this case, the sound plays afterforward-sentence
(and perhaps other advice attached to it).
- I advice the function
- Metadata about the generated functions attaches onto the config.
- hella-sounds-remove-sounds will loop through this config, looking for the metadata to remove advice to the original functions.
So really, it's just machinery over advice. All the other options are just add-ons to this general structure.
I decided not to use selectric-mode's approach1, because it hooks into post-command hook.
Challenges
Here are all the challenges I solved in making this package:
- Dynamically creating functions that had the same interactive form as the adviced function.
- I took a hot tip from
nadvice.el
which calls(interactive-form fn)
. This let me create a function with the same interacive form. - I also avoided
defmacro
by using Stefan Monnier's tip to use adefalias
on a lambda.
- I took a hot tip from
- small odds and ends:
- Did you know there are hooks you can run when a process emacs is listening to over a proc buffer are killed? It took me hours to find this solution and get it working
- I was trying to use
kill-buffer-query-functions
, which only runs when the buffer is killed. Andprocess-kill-buffer-query-function
, which didn't seem to really run? YMMV.
- I was trying to use
- Thanks to stackoverflow, I found out there's an info page on sentinels, which acts kind of like the hook mechanism but you can only have one.
(process-sentinel proc)
gives you the sentinel.
(defun hella-sounds--add-cleanup-sentinel (&optional buffer) "When the process of the BUFFER is killed, remove sounds. BUFFER should usually be the `hella-sounds-buffer'. This function calles `hella-sounds-remove-sounds' " (let* ((proc (get-buffer-process buffer)) (sentinel (process-sentinel proc))) (set-process-sentinel proc (lambda (process signal) ;; Call original sentinel, as there can only be one (funcall sentinel process signal) ;; Remove hella-sounds when the process is done (when (memq (process-status process) '(exit signal)) (hella-sounds-remove-sounds nil (buffer-local-value 'hella-sounds-config hella-sounds-buffer)))))))
- Did you know there are hooks you can run when a process emacs is listening to over a proc buffer are killed? It took me hours to find this solution and get it working
Looking ahead
I plan to prioritize work into this package if I can get at least 5 users, myself not included. I've included notes.org for certain items I think are important. But without user input, I'd be hazarding prioritizing only my own use cases.
- Performance
- at least for snd, it seems to be reading from disc at times and not reading an audio file from memory.
- I wonder if I need to write my own C program that can talk to jack/alsa/pipewire or something to do exactly what I want it to do.
- at least for snd, it seems to be reading from disc at times and not reading an audio file from memory.
- Seriously consider moving to a DFS traversal instead of hard coded loops for the init function, which makes the config closer to a customizable datastructure.
- Supporting more backends than snd,
- making the package not
snd
-specific (a lot of refactoring) - add a backend like: midio for fluidsynth, clojure's overtone, etc.
- making the package not
- Tech debt:
- In the code I need to stick to one pattern in defining the anonymous functions. To support sounds only firing in certain modes, I wrapped the lambda in another lambda. To
- In the code I need to stick to one pattern in defining the anonymous functions. To support sounds only firing in certain modes, I wrapped the lambda in another lambda. To
- ergonomics and helpers to create configs
- it can be tedious to do all that typing for sound files and functions,
- and then update the data structure in the right place
- adding a new sound to a function on impulse (impulse as in impulse shopping)
- saving currently loaded config into a file / customize variable.
- This lets you try out sound attachments and see if you like them
- and if you do, you can save them
- or let you remove all the non-saved ones.
- This lets you try out sound attachments and see if you like them
- it can be tedious to do all that typing for sound files and functions,
- Things that seem really hard, and this package doesn't attempt to do:
- knowing when a sound finishes. Important if you want to react to the state of audio. This is difficult because emacs sends the sound off to be played by a different program.
- knowing when a sequence of actions occurs. Technically you'd have to add state, but I have nothing so far that helps manage state, though you can override the default handler by supplying your own
:handler
function.
- I'm planning to work on my next video-game inspired tooling package in February though.
- I'm planning to work on my next video-game inspired tooling package in February though.
- knowing when a sound finishes. Important if you want to react to the state of audio. This is difficult because emacs sends the sound off to be played by a different program.
- Pulling out
sndd.el
from thehella-sounds.el
repo. sndd.el already has its own repository but it's not on melpa, meaninghella-sound.el
cannot require it
Selectric-mode takes a different approach:
- It creates a hashmap, say
h
- generates a lambda that wraps the callable bound to your keybindings
- replaces your keybinding callable with the lamda
- then
h
stores data to restore your originaly keybindings when selectric mode is turned off.