screwlisp proposes kittens

McCLIM picture gnuplot slideshow + eepitch two alternating slime buffers

Today Eduardo was helping me plan two eepitch buffer communication on the Mastodon.

The results are this example where one sbcl common lisp eepitch target gnuplots sine waves while another ecl common lisp eepitch target runs McCLIM to display them.

The image display in McCLIM application-frames is very simple. My reference was mdh’s https://gitlab.com/mdhughes/arrokoth .

I ran out of steam while making this article (I had a day.), but hopefully that gives you some interesting transparency into my inner workings as well.

A lisp image producing a sine wave

Setup slime+eepitch

• (setq *inferior-lisp-program* "sbcl")
• (slime)
• (setq eepitch-buffer-name "*slime-repl sbcl*")
(uiop:chdir "~/")
(uiop:chdir "~/")

A class for generating sine waves

• (setq eepitch-buffer-name "*slime-repl sbcl*")

(defclass wave ()
  ((frequency	:initarg :frequency	:accessor frequency)
   (magnitude	:initarg :magnitude	:accessor magnitude)
   (offset	:initarg :offset	:accessor offset)
   (steps	:initarg :steps		:accessor steps)
   (samples	:initform nil		:reader samples))
  (:default-initargs
   :frequency	1/100
    :magnitude	100
    :offset	0
    :steps	10)
  (:documentation ""))

(defmethod resample
    ((obj wave))
  (with-slots
	(frequency magnitude offset steps samples)
      obj
    (loop
      :for x :from offset :below (+ offset steps)
      :for y := (* magnitude (sin (* x frequency 2 pi)))
      :collect (list x (round y))
	:into results
      :finally
	 (setf samples results))))

;; samples is calculated initially
(defmethod shared-initialize
    :after 
    ((obj wave) slot-names &rest initargs &key &allow-other-keys)
  (declare (ignore initargs slot-names))
  (resample obj))

;; Resample after any change.
;; (frequency magnitude offset steps samples)
(defmethod (setf frequency)	:after (val (obj wave)) (resample obj))
(defmethod (setf magnitude)	:after (val (obj wave)) (resample obj))
(defmethod (setf offset)	:after (val (obj wave)) (resample obj))
(defmethod (setf steps)		:after (val (obj wave)) (resample obj))

Okay, αsin(2πν(x+φ)) or something, right? I added rounding because I don’t want to see or think about the floats.

TODO: Explain deffing setfs in lisp, also method qualifiers.

(make-instance 'wave)
(samples *)

#<STANDARD-METHOD (COMMON-LISP:SETF COMMON-LISP-USER::STEPS) :AFTER (T WAVE) {1004147933}>
CL-USER> (make-instance 'wave)
#<WAVE {1004149B13}>
CL-USER> (samples *)
((0 0) (1 6) (2 13) (3 19) (4 25) (5 31) (6 37) (7 43) (8 48) (9 54))
CL-USER> 

This looks good to me so far.

(* 100 (sin (* 3 2 pi 1/100)))

is 18.7 (rounds to 19).

** Check cosine

If I have divided into 100 steps, I guess cosine would be sine with an :offset of 25.

(defparameter *cosine* (make-instance 'wave :offset 25))
(samples *cosine*)

CL-USER> (defparameter *cosine* (make-instance 'wave :offset 25))
*COSINE*
CL-USER> (samples *cosine*)
((25 100) (26 100) (27 99) (28 98) (29 97) (30 95) (31 93) (32 90) (33 88)
 (34 84))

This accelerates downwards from the peak of default :magnitude 100, which is what cosine should do.

A gnuplot-samples mixin

(defclass gnuplot-mixin () ((output-file :initarg :output-file :accessor output-file))
  (:documentation
   "gnuplot-samples mixin writes SAMPLES to OUTPUT-FILE :after RESAMPLE using gnuplot/png."))

(defmethod resample
    :after
    ((obj gnuplot-mixin))
  (with-slots
	(samples output-file)
      obj
    (with-input-from-string
	(in (format nil
		    "~{~{~d ~d~}~%~}e"
		    samples))
      (uiop:run-program
       (format nil
	       "gnuplot -e '~@{~?;~^ ~}'"
	       "set terminal png" ()
	       "set output ~s" `(,output-file)
	       "plot ~s using 1:2 with lines" `("-"))
       :input in))))

Gnuplotted wave segment

(defclass gwave (gnuplot-mixin wave) ())
(make-instance 'gwave :output-file "out.png")
(setf (offset *) 25)

Looking good, but we’re still at a single emacs user driving a single lisp image driving gnuplot outputs to PNG. I thought about adding “the usual” additional keys to gnuplot, but I was reminded of Ken Pitman’s memory of Richard Waters, I think, saying “A function that takes seven arguments does not take enough arguments”, meaning that if the user is explicitly passing these seven arguments, surely they could explicitly pass a few more (and a few more). Functions should not take more arguments than humans can reasonably have in their head at once.

So if I add a couple more arguments, surely there are more arguments I should also add, and that way madness lies. Instead, I would like to embrace the automatic, dare I say good-old-fashioned-AI behaviours in lieu of arguements.

A McCLIM app displaying pngs

Instead of driving the foreign gnuplot as a spawned process, I am adding a lisp-native McCLIM image displayer in another frame. This gets us closer to Edrx’s request of seeing three way slime intercommunication via emacs eev eepitch.

There is also a general urgency to adding image rendering / operations to all my symbolic graphical user interfaces.

I’m going to use embeddable common lisp to host McCLIM, whereas we are currently using steel bank common lisp for crunching-our-numbers (er, computing a sine).

• (setq inferior-lisp-program "ecl")
• (setq eepitch-buffer-name "*slime-repl ECL*")
• (slime)

(require :mcclim)
(in-package :clim-user)

so we have a second slime eepitch target. To display images, I am refering to mdhughes’ arrokoth git’s render-bitmap, though I’m doing a less rigorous job here and now.

Let’s just look at one picture for now.

(define-application-frame picture-frame ()
  ((file :initarg :file))
  (:pane :application :display-function 'display-picture)
  (:default-initargs :file "out.png"))

(defun display-picture (frame pane)
  (with-slots (file) frame
    (let ((pattern (make-pattern-from-bitmap-file file)))
      (draw-pattern* pane pattern 0 0))))

(find-application-frame 'picture-frame)

Let’s just see that working.

Clearly McCLIM / the common lisp interface manager spec makes it very easy to load and display pictures like PNGs. In our case, the :display-function always loads (and decompresses) the file, which takes a long time to do. However in the default settings, redisplays only happen after a command (i.e. changes are expected to be done by commands), so this is not a problem here.

A command to set file

(define-picture-frame-command
    (com-set-file :name t)
    ((image-file 'string))
  (with-slots (file) *application-frame*
    (setf file image-file)))

When this is issues, the display function (more or less) should be reevaluated. I guess we’ll use it like

• (setq eepitch-buffer-name "*slime-repl ECL*")
(defparameter *picture-frame* (make-application-frame 'picture-frame :file "out.png"))

(bt:make-thread
 (lambda () (run-frame-top-level *picture-frame*)))

(execute-frame-command *picture-frame*
		       '(com-set-file "out.png"))

Alternately eepitching to two slime repls

• (setq eepitch-buffer-name "*slime-repl sbcl*")
(defparameter *wave* (make-instance 'gwave :output-file
				    "out.png"))

• (setq eepitch-buffer-name "*slime-repl ECL*")
(execute-frame-command *picture-frame*
		       '(com-set-file "out.png"))

• (setq eepitch-buffer-name "*slime-repl sbcl*")
(setf (offset *wave*) 9)

• (setq eepitch-buffer-name "*slime-repl ECL*")
(execute-frame-command *picture-frame*
		       '(com-set-file "out.png"))

• (setq eepitch-buffer-name "*slime-repl sbcl*")
(setf (offset *wave*) 18)

• (setq eepitch-buffer-name "*slime-repl ECL*")
(execute-frame-command *picture-frame*
		       '(com-set-file "out.png"))

Intertarget eepitching

This is cool and all in that it showed me alternating the eepitch targets on different parts of the screen (awkwardly. WIP).

However, what if I only talked to one target, and that target talked to the other target on the screen.

• (server-start)
• (setq eepitch-buffer-name "*slime-repl sbcl*")
(defclass eepitch ()
  ((target1 :initarg :target1)
   (target2 :initarg :target2))
  (:default-initargs :target1 "*slime-repl ECL*"
		     :target2 "*slime-repl sbcl*"))

(defmethod send ((obj eepitch) string)
  (uiop:run-program
   (format nil "emacsclient -e ~s" string)))

(defmethod send :before ((obj eepitch) string)
  (with-slots (target1) obj
    (uiop:run-program
     (format nil "emacsclient -e '~?'"
	     "(setq eepitch-buffer-name ~s)" `(,target1)))))

(defmethod send :after ((obj eepitch) string)
  (with-slots (target2) obj
    (uiop:run-program
     (format nil "emacsclient -e '~?'"
	     "(setq eepitch-buffer-name ~s)" `(,target2)))))

Test that.

(defparameter *sender* (make-instance 'eepitch))
(send *sender* "(setq foo 'bar)")
• foo

Well, looks like that works.

Oh, you know what, I ran out of steam. It’s been a difficult day outside of this article, and edrx is hopefully asleep by now anyway.

Conclusions

We saw

  1. McCLIM images, finally, thanks to https://mdhughes.tech .
  2. eepitching with two buffers, finally, thanks https://angtwu.net
  3. eepitching to two targets, one running our McCLIM picture-frame, one generating pictures
  4. A class that uses emacsclient to eval elisp in emacs.

However I ran out of steam before the eepitch emacsclient-using lisp class started using eepitch-send itself.

My vision (and emacsconf talk this year) will be about three-way communication including a human and two lisp images, which we can see a glimmer of here. However, the lisp images should be chattering back and forth with each other a lot. Near-field work.

Fin.

Mastodon thread please. It turns out I need to travel this weekend, so I’m not sure what my availability will be but I look forward to hearing your thoughts on pictures and eepitching everyone.

screwlisp proposes kittens