Monorepo for Aesthetic.Computer
aesthetic.computer
1;;; Notepat — AC Native OS musical keyboard instrument (Common Lisp)
2;;; Port of fedac/native/pieces/notepat.mjs
3
4(in-package :ac-native)
5
6(defvar *running* t "Main loop flag.")
7
8;;; ── Pixel scale ──
9
10(defun compute-pixel-scale (display-w)
11 "Compute pixel scale targeting ~200px wide (bigger pixels)."
12 (let ((target (max 1 (min 16 (floor display-w 200)))))
13 (loop for delta from 0 to 3 do
14 (let ((s (+ target delta)))
15 (when (and (>= s 1) (<= s 16) (zerop (mod display-w s)))
16 (return-from compute-pixel-scale s)))
17 (let ((s (- target delta)))
18 (when (and (>= s 1) (zerop (mod display-w s)))
19 (return-from compute-pixel-scale s))))
20 target))
21
22;;; ── Music theory ──
23
24(defvar *chromatic* #("c" "c#" "d" "d#" "e" "f" "f#" "g" "g#" "a" "a#" "b"))
25
26(defun note-to-freq (note-name octave)
27 "Convert note name and octave to frequency in Hz. A4 = 440Hz."
28 (let ((idx (position note-name *chromatic* :test #'string=)))
29 (if idx
30 (* 440.0d0 (expt 2.0d0 (+ (- octave 4) (/ (- idx 9) 12.0d0))))
31 440.0d0)))
32
33(defun note-is-sharp-p (note-name)
34 (search "#" note-name))
35
36;;; ── Note colors (chromatic rainbow) ──
37
38(defvar *note-colors*
39 '(("c" 255 30 30) ; red
40 ("c#" 255 80 0) ; red-orange
41 ("d" 255 150 0) ; orange
42 ("d#" 200 200 0) ; yellow-green
43 ("e" 230 220 0) ; yellow
44 ("f" 30 200 30) ; green
45 ("f#" 0 200 180) ; teal
46 ("g" 30 100 255) ; blue
47 ("g#" 80 50 255) ; indigo
48 ("a" 140 30 220) ; purple
49 ("a#" 200 30 150) ; magenta
50 ("b" 200 50 255))) ; violet
51
52(defun note-color-rgb (note-name)
53 "Return (r g b) list for a note name."
54 (let ((entry (assoc note-name *note-colors* :test #'string=)))
55 (if entry (cdr entry) '(80 80 80))))
56
57;;; ── Keyboard mapping: evdev keycode → (note-name . octave-offset) ──
58
59(defvar *key-note-map* (make-hash-table :test 'eql))
60
61(defun init-key-note-map ()
62 "Populate keycode → note mapping (QWERTY layout matching JS notepat)."
63 (clrhash *key-note-map*)
64 (flet ((m (key note off) (setf (gethash key *key-note-map*) (cons note off))))
65 ;; Lower octave naturals
66 (m ac-native.input:+key-c+ "c" 0) (m ac-native.input:+key-d+ "d" 0)
67 (m ac-native.input:+key-e+ "e" 0) (m ac-native.input:+key-f+ "f" 0)
68 (m ac-native.input:+key-g+ "g" 0) (m ac-native.input:+key-a+ "a" 0)
69 (m ac-native.input:+key-b+ "b" 0)
70 ;; Lower octave sharps
71 (m ac-native.input:+key-v+ "c#" 0) (m ac-native.input:+key-s+ "d#" 0)
72 (m ac-native.input:+key-w+ "f#" 0) (m ac-native.input:+key-r+ "g#" 0)
73 (m ac-native.input:+key-q+ "a#" 0)
74 ;; Upper octave naturals
75 (m ac-native.input:+key-h+ "c" 1) (m ac-native.input:+key-i+ "d" 1)
76 (m ac-native.input:+key-j+ "e" 1) (m ac-native.input:+key-k+ "f" 1)
77 (m ac-native.input:+key-l+ "g" 1) (m ac-native.input:+key-m+ "a" 1)
78 (m ac-native.input:+key-n+ "b" 1)
79 ;; Upper octave sharps
80 (m ac-native.input:+key-t+ "c#" 1) (m ac-native.input:+key-y+ "d#" 1)
81 (m ac-native.input:+key-u+ "f#" 1) (m ac-native.input:+key-o+ "g#" 1)
82 (m ac-native.input:+key-p+ "a#" 1)
83 ;; Extension +2
84 (m ac-native.input:+key-semicolon+ "c" 2)
85 (m ac-native.input:+key-apostrophe+ "c#" 2)
86 (m ac-native.input:+key-rightbrace+ "d" 2)
87 ;; Sub-octave
88 (m ac-native.input:+key-z+ "a#" -1) (m ac-native.input:+key-x+ "b" -1)))
89
90;;; ── State ──
91
92(defvar *wave-names* #("sine" "triangle" "sawtooth" "square" "noise"))
93(defvar *wave-index* 0)
94(defvar *octave* 4)
95(defvar *quick-mode* nil "Short attack/release for staccato play.")
96
97;; Active voices and trails
98(defvar *active-voices* (make-hash-table :test 'eql))
99(defvar *active-notes* (make-hash-table :test 'eql))
100(defvar *trails* (make-hash-table :test 'equal))
101
102;; Background color
103(defvar *bg-r* 20) (defvar *bg-g* 20) (defvar *bg-b* 25)
104
105;; FPS
106(defvar *fps-display* 0)
107(defvar *fps-accum* 0.0d0)
108(defvar *fps-samples* 0)
109(defvar *fps-last-time* 0.0d0)
110
111;; ESC triple-press
112(defvar *esc-count* 0)
113(defvar *esc-last-frame* 0)
114
115;; Metronome
116(defvar *metronome-on* nil)
117(defvar *metronome-bpm* 120)
118(defvar *metronome-last-beat* -1)
119(defvar *metronome-flash* 0.0 "Visual flash intensity 0-1, decays per frame.")
120(defvar *metronome-phase* 0.0 "Pendulum swing -1..1.")
121
122;; Identity
123(defvar *boot-handle* nil "Handle from config, set during boot splash.")
124
125;; Native notepat runtime state (defvar — SBCL standalone loses let* locals)
126(defvar *np-dw* 0)
127(defvar *np-dh* 0)
128(defvar *np-scale* 1)
129(defvar *np-sw* 0)
130(defvar *np-sh* 0)
131(defvar *np-screen* nil)
132(defvar *np-graph* nil)
133(defvar *np-input* nil)
134(defvar *np-audio* nil)
135(defvar *np-frame* 0)
136(defvar *np-display* nil)
137
138;; Network
139(defvar *ip-address* "")
140
141(defun refresh-ip ()
142 (handler-case
143 (let ((output (with-output-to-string (s)
144 (sb-ext:run-program "/sbin/ip" '("-4" "-o" "addr" "show")
145 :output s :error nil))))
146 (dolist (line (uiop:split-string output :separator '(#\Newline)))
147 (when (and (search "inet " line) (not (search "127.0.0.1" line)))
148 (let* ((inet-pos (search "inet " line))
149 (ip-start (+ inet-pos 5))
150 (slash-pos (position #\/ line :start ip-start)))
151 (when slash-pos
152 (setf *ip-address* (subseq line ip-start slash-pos))
153 (return))))))
154 (error () nil)))
155
156;;; ── Helpers ──
157
158(defun kill-all-voices (audio)
159 "Kill all active voices (on octave/wave change)."
160 (when audio
161 (maphash (lambda (code voice-id)
162 (declare (ignore code))
163 (audio-synth-kill audio voice-id))
164 *active-voices*))
165 (clrhash *active-voices*)
166 (clrhash *active-notes*))
167
168;;; ── Build metadata ──
169
170(defvar *build-name* "dev" "Build name from /etc/ac-build.")
171(defvar *build-variant* "c" "Build variant: c or cl.")
172
173(defun load-build-metadata ()
174 "Read /etc/ac-build: line 1=name, 2=hash, 3=timestamp, 4=variant."
175 (handler-case
176 (when (probe-file "/etc/ac-build")
177 (with-open-file (s "/etc/ac-build" :direction :input)
178 (let ((name (read-line s nil nil))
179 (hash (read-line s nil nil))
180 (ts (read-line s nil nil))
181 (variant (read-line s nil nil)))
182 (declare (ignore hash ts))
183 (when name (setf *build-name* name))
184 (when variant (setf *build-variant* variant)))))
185 (error () nil)))
186
187;;; ── Boot splash ──
188
189(defun time-greeting ()
190 "Return time-of-day greeting string."
191 (let ((hour (nth-value 2 (get-decoded-time))))
192 (cond ((and (>= hour 5) (< hour 12)) "good morning")
193 ((and (>= hour 12) (< hour 17)) "good afternoon")
194 (t "good evening"))))
195
196;;; ── JS Piece Runner ──
197
198(defun main-js (piece-path)
199 "Run a .mjs piece via the QuickJS bridge."
200 (format *error-output* "~%════════════════════════════════════~%")
201 (format *error-output* " AC Native OS (Common Lisp + QuickJS)~%")
202 (format *error-output* " SBCL ~A~%" (lisp-implementation-version))
203 (format *error-output* " piece: ~A~%" piece-path)
204 (format *error-output* "════════════════════════════════════~%~%")
205 (force-output *error-output*)
206
207 (let ((display (handler-case (ac-native.drm:drm-init)
208 (error (e)
209 (format *error-output* "[js] DRM error: ~A~%" e)
210 (force-output *error-output*) nil))))
211 (unless display
212 (format *error-output* "[js] FATAL: no display~%")
213 (force-output *error-output*) (sleep 30) (return-from main-js 1))
214
215 (let* ((dw (ac-native.drm:display-width display))
216 (dh (ac-native.drm:display-height display))
217 (scale (compute-pixel-scale dw))
218 (sw (floor dw scale))
219 (sh (floor dh scale))
220 (screen (fb-create sw sh))
221 (graph (make-graph :fb screen :screen screen))
222 (input (ac-native.input:input-init dw dh scale))
223 (audio (ac-native.audio:audio-init))
224 (frame 0))
225
226 (font-init)
227
228 ;; Initialize QuickJS bridge
229 (handler-case
230 (js-init graph screen audio sw sh)
231 (error (e)
232 (format *error-output* "[js] JS-INIT ERROR: ~A~%" e)
233 (force-output *error-output*)
234 (when audio (audio-destroy audio))
235 (ac-native.input:input-destroy input)
236 (fb-destroy screen)
237 (ac-native.drm:drm-destroy display)
238 (setf *js-fallback* t)
239 (return-from main-js (main))))
240
241 ;; Load the piece
242 (unless (js-load-piece piece-path)
243 (format *error-output* "[js] Failed to load piece, falling back to native notepat~%")
244 (force-output *error-output*)
245 (js-destroy)
246 ;; Cleanup and fall through to native
247 (when audio (audio-destroy audio))
248 (ac-native.input:input-destroy input)
249 (fb-destroy screen)
250 (ac-native.drm:drm-destroy display)
251 (setf *js-fallback* t)
252 (return-from main-js (main)))
253
254 ;; Call boot
255 (js-boot)
256
257 ;; Start Swank
258 (handler-case
259 (progn
260 (setf swank::*communication-style* :spawn)
261 (swank:create-server :port 4005 :dont-close t)
262 (format *error-output* "[js] Swank on :4005~%")
263 (force-output *error-output*))
264 (error (e)
265 (format *error-output* "[js] Swank failed: ~A~%" e)
266 (force-output *error-output*)))
267
268 ;; Main loop
269 (setf *running* t)
270 (unwind-protect
271 (loop while *running* do
272 (incf frame)
273
274 ;; Input → act
275 (dolist (ev (ac-native.input:input-poll input))
276 (let ((type (ac-native.input:event-type ev))
277 (code (ac-native.input:event-code ev))
278 (key (ac-native.input:event-key ev)))
279 ;; ESC triple-press to quit
280 (when (and (eq type :key-down) (= code ac-native.input:+key-esc+))
281 (when (> (- frame *esc-last-frame*) 90) (setf *esc-count* 0))
282 (incf *esc-count*)
283 (setf *esc-last-frame* frame)
284 (when (>= *esc-count* 3) (setf *running* nil)))
285 ;; Power button
286 (when (and (eq type :key-down) (= code ac-native.input:+key-power+))
287 (setf *running* nil))
288 ;; Pass to JS
289 (js-act (if (eq type :key-down) 1 0)
290 (or key "") code)))
291
292 ;; sim
293 (js-sim)
294
295 ;; paint
296 (js-paint frame)
297
298 ;; Check for jump or poweroff from JS
299 (when ac-native.js-bridge::*poweroff-requested*
300 (setf *running* nil))
301 (when ac-native.js-bridge::*jump-target*
302 ;; TODO: reload piece
303 (format *error-output* "[js] jump to ~A (not yet implemented)~%"
304 ac-native.js-bridge::*jump-target*)
305 (force-output *error-output*)
306 (setf ac-native.js-bridge::*jump-target* nil))
307
308 ;; Present
309 (ac-native.drm:drm-present display screen scale)
310 (frame-sync-60fps))
311
312 ;; Cleanup
313 (js-destroy)
314 (when audio (audio-destroy audio))
315 (ac-native.input:input-destroy input)
316 (fb-destroy screen)
317 (ac-native.drm:drm-destroy display)
318 (format *error-output* "[js] shutdown~%")
319 (force-output *error-output*)))))
320
321;;; ── Main ──
322
323(defvar *js-fallback* nil "Set to T when falling back from JS to prevent re-entry loop.")
324
325(defun run-cl-piece (paint-fn piece-name)
326 "Run a CL piece with DRM graphics, audio, and input. PAINT-FN is called each frame."
327 (format *error-output* "~%════════════════════════════════════~%")
328 (format *error-output* " ~A (Common Lisp)~%" piece-name)
329 (format *error-output* "════════════════════════════════════~%~%")
330 (force-output *error-output*)
331 (let ((display (ac-native.drm:drm-init)))
332 (unless display (return-from run-cl-piece))
333 (let* ((dw (ac-native.drm:display-width display))
334 (dh (ac-native.drm:display-height display))
335 (scale (cond ((>= (min dw dh) 1440) 6)
336 ((>= (min dw dh) 1080) 4)
337 ((>= (min dw dh) 720) 3)
338 (t 2)))
339 (sw (floor dw scale))
340 (sh (floor dh scale))
341 (screen (ac-native.framebuffer:fb-create sw sh))
342 (graph (ac-native.graph:graph-create screen))
343 (input (ac-native.input:input-init dw dh scale))
344 (audio (ac-native.audio:audio-init)))
345 (unwind-protect
346 (loop
347 ;; Input
348 (ac-native.input:input-poll input)
349 (dotimes (i (ac-native.input:input-event-count input))
350 (let ((ev (ac-native.input:input-event input i)))
351 (when (and (= (ac-native.input:event-type ev) 1)
352 (= (ac-native.input:event-code ev) 1))
353 (return)))) ; ESC
354 ;; Paint
355 (handler-case
356 (funcall paint-fn graph sw sh audio)
357 (error (e)
358 (format *error-output* "[piece] error: ~A~%" e)
359 (force-output *error-output*)))
360 ;; Present
361 (ac-native.drm:drm-present display screen scale)
362 (sleep 1/60))
363 ;; Cleanup
364 (when audio (ac-native.audio:audio-destroy audio))
365 (ac-native.input:input-destroy input)
366 (ac-native.framebuffer:fb-destroy screen)
367 (ac-native.drm:drm-destroy display)))))
368
369(defun main ()
370 "AC Native OS entry point. Runs .mjs pieces via QuickJS or native CL notepat.
371When called with --swank-only, starts Swank server and blocks (no graphics)."
372 ;; Swank-only mode: just start the REPL server and sleep forever
373 (when (member "--swank-only" (uiop:command-line-arguments) :test #'string=)
374 (format *error-output* "[swank] Starting Swank server on port 4005...~%")
375 (force-output *error-output*)
376 (handler-case
377 (progn
378 (setf swank::*communication-style* :spawn)
379 (swank:create-server :port 4005 :dont-close t)
380 (format *error-output* "[swank] Swank ready on port 4005~%")
381 (force-output *error-output*)
382 ;; Block forever — Swank handles connections in its own threads
383 (loop (sleep 3600)))
384 (error (e)
385 (format *error-output* "[swank] Failed: ~A~%" e)
386 (force-output *error-output*)
387 (return-from main))))
388
389 ;; --piece MODE: run a specific CL piece with graphics (launched by C binary)
390 (let ((piece-arg (second (member "--piece" (uiop:command-line-arguments) :test #'string=))))
391 (when piece-arg
392 (format *error-output* "[cl] Running CL piece: ~A~%" piece-arg)
393 (force-output *error-output*)
394 ;; Try to load the piece file and run its paint function
395 (let ((piece-path (format nil "/pieces/~A.lisp" piece-arg)))
396 (when (probe-file piece-path)
397 (handler-case
398 (progn
399 (load piece-path)
400 (let ((paint-fn (find-symbol "PAINT" (find-package
401 (intern (string-upcase (format nil "PIECE.~A" piece-arg)) :keyword)))))
402 (when (and paint-fn (fboundp paint-fn))
403 (return-from main (run-cl-piece paint-fn piece-arg)))))
404 (error (e)
405 (format *error-output* "[cl] Error loading ~A: ~A~%" piece-path e)
406 (force-output *error-output*)))))
407 ;; Fallback to native CL notepat
408 (setf *js-fallback* t)))
409
410 ;; Determine piece: config.json > command-line arg > default
411 (unless *js-fallback*
412 (let* ((cfg (handler-case (ac-native.config:load-config)
413 (error () (ac-native.config:make-config))))
414 (config-piece (ac-native.config:config-piece cfg))
415 (args (uiop:command-line-arguments))
416 (piece-name (or config-piece
417 (when args (first args))
418 "notepat"))
419 (piece-path (if (search "/" piece-name)
420 piece-name ; already a path
421 (format nil "/pieces/~A.mjs" piece-name))))
422 (when (probe-file piece-path)
423 (return-from main (main-js piece-path)))))
424 (setf *js-fallback* nil)
425
426 ;; No .mjs piece specified — fall back to native CL notepat
427 (format *error-output* "~%════════════════════════════════════~%")
428 (format *error-output* " notepat (Common Lisp)~%")
429 (format *error-output* " SBCL ~A~%" (lisp-implementation-version))
430 (format *error-output* "════════════════════════════════════~%~%")
431 (force-output *error-output*)
432
433 (init-key-note-map)
434
435 (setf *np-display* (handler-case (ac-native.drm:drm-init)
436 (error (e)
437 (format *error-output* "[notepat] DRM error: ~A~%" e)
438 (force-output *error-output*) nil)))
439 (unless *np-display*
440 (format *error-output* "[notepat] FATAL: no display~%")
441 (force-output *error-output*) (sleep 30) (return-from main 1))
442
443 (setf *np-dw* (ac-native.drm:display-width *np-display*)
444 *np-dh* (ac-native.drm:display-height *np-display*)
445 *np-scale* (compute-pixel-scale *np-dw*)
446 *np-sw* (floor *np-dw* *np-scale*)
447 *np-sh* (floor *np-dh* *np-scale*)
448 *np-screen* (fb-create *np-sw* *np-sh*)
449 *np-graph* (make-graph :fb *np-screen* :screen *np-screen*)
450 *np-input* (ac-native.input:input-init *np-dw* *np-dh* *np-scale*)
451 *np-audio* (ac-native.audio:audio-init)
452 *np-frame* 0)
453 (progn
454
455 (format *error-output* "[notepat] ~Dx~D scale:~D → ~Dx~D~%"
456 *np-dw* *np-dh* *np-scale* *np-sw* *np-sh*)
457 (format *error-output* "[notepat] audio: ~A~%"
458 (if *np-audio* "OK" "FAILED"))
459 (force-output *error-output*)
460
461 (font-init)
462 (load-build-metadata)
463
464 ;; ── Boot splash ──
465 (let* ((cfg (ac-native.config:load-config))
466 (handle (ac-native.config:config-handle cfg))
467 (has-handle (and handle (string/= handle "") (string/= handle "unknown")))
468 (greeting (time-greeting))
469 (splash-start (monotonic-time-ms)))
470 ;; Write tokens
471 (handler-case (ac-native.config:write-device-tokens cfg)
472 (error (e)
473 (format *error-output* "[notepat] token write error: ~A~%" e)
474 (force-output *error-output*)))
475 ;; Store handle for status display
476 (setf *boot-handle* (if has-handle handle nil))
477 ;; Show splash for 3 seconds or until keypress
478 (loop while (< (- (monotonic-time-ms) splash-start) 3000) do
479 (let ((events (ac-native.input:input-poll *np-input*)))
480 (when (some (lambda (ev) (eq (ac-native.input:event-type ev) :key-down)) events)
481 (return)))
482 ;; Paint splash
483 (graph-wipe *np-graph* (make-color :r 10 :g 12 :b 18))
484 (let ((cy (floor *np-sh* 3)))
485 (if has-handle
486 (progn
487 ;; Greeting
488 (graph-ink *np-graph* (make-color :r 140 :g 160 :b 200 :a 220))
489 (font-draw *np-graph* greeting
490 (- (floor *np-sw* 2) (floor (font-measure greeting) 2)) cy)
491 ;; @handle
492 (let ((htxt (format nil "@~A" handle)))
493 (graph-ink *np-graph* (make-color :r 80 :g 255 :b 140 :a 255))
494 (font-draw *np-graph* htxt
495 (- (floor *np-sw* 2) (floor (font-measure htxt) 2)) (+ cy 14)))
496 ;; Subtitle
497 (graph-ink *np-graph* (make-color :r 80 :g 80 :b 100 :a 160))
498 (font-draw *np-graph* "aesthetic.computer"
499 (- (floor *np-sw* 2) (floor (font-measure "aesthetic.computer") 2)) (+ cy 32)))
500 (progn
501 ;; No handle: just show name
502 (graph-ink *np-graph* (make-color :r 80 :g 255 :b 140 :a 255))
503 (font-draw *np-graph* "aesthetic.computer"
504 (- (floor *np-sw* 2) (floor (font-measure "aesthetic.computer") 2)) cy)
505 (graph-ink *np-graph* (make-color :r 140 :g 160 :b 200 :a 180))
506 (font-draw *np-graph* "notepat"
507 (- (floor *np-sw* 2) (floor (font-measure "notepat") 2)) (+ cy 18)))))
508 ;; Build name (bottom center)
509 (graph-ink *np-graph* (make-color :r 60 :g 60 :b 80 :a 120))
510 (font-draw *np-graph* *build-name*
511 (- (floor *np-sw* 2) (floor (font-measure *build-name*) 2)) (- *np-sh* 20))
512 ;; LISP tag (top right) when CL variant
513 (when (string= *build-variant* "cl")
514 (graph-ink *np-graph* (make-color :r 255 :g 200 :b 80 :a 200))
515 (font-draw *np-graph* "LISP" (- *np-sw* (font-measure "LISP") 6) 6)))
516 (ac-native.drm:drm-present *np-display* *np-screen* *np-scale*)
517 (frame-sync-60fps)))
518
519 ;; Start Swank server for remote REPL
520 (handler-case
521 (progn
522 (setf swank::*communication-style* :spawn)
523 (swank:create-server :port 4005 :dont-close t)
524 (format *error-output* "[notepat] Swank on :4005~%")
525 (force-output *error-output*))
526 (error (e)
527 (format *error-output* "[notepat] Swank failed: ~A~%" e)
528 (force-output *error-output*)))
529
530 ;; Main loop
531 (setf *running* t)
532 (unwind-protect
533 (loop while *running* do
534 (incf *np-frame*)
535
536 ;; FPS
537 (let ((now (monotonic-time-ms)))
538 (when (> *fps-last-time* 0.0d0)
539 (incf *fps-accum* (- now *fps-last-time*))
540 (incf *fps-samples*)
541 (when (>= *fps-samples* 30)
542 (setf *fps-display* (round (/ 30000.0d0 *fps-accum*)))
543 (setf *fps-accum* 0.0d0 *fps-samples* 0)))
544 (setf *fps-last-time* now))
545
546 ;; ── Metronome tick ──
547 (when (and *metronome-on* (> *metronome-bpm* 0) *np-audio*)
548 (let* ((now-ms (monotonic-time-ms))
549 (ms-per-beat (/ 60000.0d0 *metronome-bpm*))
550 (beat-number (floor now-ms ms-per-beat))
551 ;; Pendulum: sinusoidal swing over 2-beat period
552 (beat-phase (/ (mod now-ms (* ms-per-beat 2)) (* ms-per-beat 2))))
553 (setf *metronome-phase* (sin (* beat-phase pi 2.0d0)))
554 (when (/= beat-number *metronome-last-beat*)
555 (setf *metronome-last-beat* beat-number)
556 (setf *metronome-flash* 1.0)
557 (let ((downbeat (zerop (mod beat-number 4))))
558 (audio-synth *np-audio* :type 3 ; square
559 :tone (if downbeat 1200.0d0 800.0d0)
560 :duration 0.03d0
561 :volume (if downbeat 0.4d0 0.25d0)
562 :attack 0.001d0 :decay 0.02d0)))))
563
564 ;; Decay metronome flash
565 (when (> *metronome-flash* 0.0)
566 (decf *metronome-flash* 0.15)
567 (when (< *metronome-flash* 0.0) (setf *metronome-flash* 0.0)))
568
569 ;; ── Input ──
570 (dolist (ev (ac-native.input:input-poll *np-input*))
571 (let ((type (ac-native.input:event-type ev))
572 (code (ac-native.input:event-code ev)))
573
574 (when (eq type :key-down)
575 ;; ESC: triple-press to quit
576 (when (= code ac-native.input:+key-esc+)
577 (when (> (- *np-frame* *esc-last-frame*) 90) (setf *esc-count* 0))
578 (incf *esc-count*)
579 (setf *esc-last-frame* *np-frame*)
580 (when (and *np-audio* (< *esc-count* 3))
581 (audio-synth *np-audio* :type 3
582 :tone (if (= *esc-count* 1) 440.0d0 660.0d0)
583 :duration 0.08d0 :volume 0.15d0
584 :attack 0.002d0 :decay 0.06d0))
585 (when (>= *esc-count* 3) (setf *running* nil)))
586
587 ;; Power
588 (when (= code ac-native.input:+key-power+) (setf *running* nil))
589
590 ;; Shift: toggle quick mode
591 (when (= code 42) ; KEY_LEFTSHIFT
592 (setf *quick-mode* (not *quick-mode*)))
593
594 ;; Space: toggle metronome
595 (when (= code ac-native.input:+key-space+)
596 (setf *metronome-on* (not *metronome-on*))
597 (when *metronome-on*
598 (setf *metronome-last-beat* -1)))
599
600 ;; Minus / Equal: BPM control
601 (when (= code ac-native.input:+key-minus+)
602 (setf *metronome-bpm* (max 20 (- *metronome-bpm* 5))))
603 (when (= code ac-native.input:+key-equal+)
604 (setf *metronome-bpm* (min 300 (+ *metronome-bpm* 5))))
605
606 ;; Number keys: set octave (kills active voices)
607 (when (and (>= code ac-native.input:+key-1+)
608 (<= code ac-native.input:+key-9+))
609 (let ((new-oct (1+ (- code ac-native.input:+key-1+))))
610 (unless (= new-oct *octave*)
611 (kill-all-voices *np-audio*)
612 (setf *octave* new-oct))))
613
614 ;; Arrow up/down: octave
615 (when (= code ac-native.input:+key-up+)
616 (when (< *octave* 9)
617 (kill-all-voices *np-audio*)
618 (incf *octave*)))
619 (when (= code ac-native.input:+key-down+)
620 (when (> *octave* 1)
621 (kill-all-voices *np-audio*)
622 (decf *octave*)))
623
624 ;; Tab / Arrow left/right: cycle wave type
625 (when (or (= code ac-native.input:+key-tab+)
626 (= code ac-native.input:+key-right+))
627 (kill-all-voices *np-audio*)
628 (setf *wave-index* (mod (1+ *wave-index*) 5))
629 (when *np-audio*
630 (let ((tones #(660.0d0 550.0d0 440.0d0 330.0d0 220.0d0)))
631 (audio-synth *np-audio* :type *wave-index*
632 :tone (aref tones *wave-index*)
633 :duration 0.07d0 :volume 0.18d0
634 :attack 0.002d0 :decay 0.06d0))))
635 (when (= code ac-native.input:+key-left+)
636 (kill-all-voices *np-audio*)
637 (setf *wave-index* (mod (+ *wave-index* 4) 5))
638 (when *np-audio*
639 (let ((tones #(660.0d0 550.0d0 440.0d0 330.0d0 220.0d0)))
640 (audio-synth *np-audio* :type *wave-index*
641 :tone (aref tones *wave-index*)
642 :duration 0.07d0 :volume 0.18d0
643 :attack 0.002d0 :decay 0.06d0))))
644
645 ;; Note keys
646 (let ((mapping (gethash code *key-note-map*)))
647 (when (and mapping (not (gethash code *active-voices*)) *np-audio*)
648 (let* ((note-name (car mapping))
649 (oct-delta (cdr mapping))
650 (actual-octave (+ *octave* oct-delta))
651 (freq (note-to-freq note-name actual-octave))
652 (idx (position note-name *chromatic* :test #'string=))
653 (semitones (+ (* (- actual-octave 4) 12) (or idx 0)))
654 (pan (max -0.8d0 (min 0.8d0 (/ (- semitones 12) 15.0d0))))
655 (attack (if *quick-mode* 0.002d0 0.005d0))
656 (voice-id (audio-synth *np-audio*
657 :type *wave-index*
658 :tone freq
659 :volume 0.7d0
660 :duration 0
661 :attack attack
662 :decay 0.1d0
663 :pan pan)))
664 (setf (gethash code *active-voices*) voice-id)
665 (setf (gethash code *active-notes*)
666 (cons note-name actual-octave))))))
667
668 ;; Key up
669 (when (eq type :key-up)
670 (let ((voice-id (gethash code *active-voices*)))
671 (when (and voice-id *np-audio*)
672 (audio-synth-kill *np-audio* voice-id)
673 (remhash code *active-voices*)
674 (let ((note-info (gethash code *active-notes*)))
675 (when note-info
676 (setf (gethash note-info *trails*) 1.0)
677 (remhash code *active-notes*))))))))
678
679 ;; ── Trail decay ──
680 (let ((dead nil))
681 (maphash (lambda (note val)
682 (let ((new-val (- val 0.025)))
683 (if (<= new-val 0.0)
684 (push note dead)
685 (setf (gethash note *trails*) new-val))))
686 *trails*)
687 (dolist (n dead) (remhash n *trails*)))
688
689 ;; ── Background color from active notes ──
690 (let ((n (hash-table-count *active-notes*)))
691 (if (> n 0)
692 (let ((tr 0) (tg 0) (tb 0))
693 (maphash (lambda (code note-info)
694 (declare (ignore code))
695 (let ((rgb (note-color-rgb (car note-info))))
696 (incf tr (first rgb))
697 (incf tg (second rgb))
698 (incf tb (third rgb))))
699 *active-notes*)
700 (let ((target-r (floor (* (floor tr n) 35) 100))
701 (target-g (floor (* (floor tg n) 35) 100))
702 (target-b (floor (* (floor tb n) 35) 100)))
703 (setf *bg-r* (+ *bg-r* (floor (- target-r *bg-r*) 4)))
704 (setf *bg-g* (+ *bg-g* (floor (- target-g *bg-g*) 4)))
705 (setf *bg-b* (+ *bg-b* (floor (- target-b *bg-b*) 4)))))
706 (progn
707 (setf *bg-r* (+ *bg-r* (floor (- 20 *bg-r*) 8)))
708 (setf *bg-g* (+ *bg-g* (floor (- 20 *bg-g*) 8)))
709 (setf *bg-b* (+ *bg-b* (floor (- 25 *bg-b*) 8))))))
710
711 ;; Metronome flash brightens background
712 (when (> *metronome-flash* 0.0)
713 (let ((boost (floor (* *metronome-flash* 40))))
714 (setf *bg-r* (min 255 (+ *bg-r* boost)))
715 (setf *bg-g* (min 255 (+ *bg-g* boost)))
716 (setf *bg-b* (min 255 (+ *bg-b* boost)))))
717
718 ;; ══════════════ PAINT ══════════════
719 (graph-wipe *np-graph* (make-color :r *bg-r* :g *bg-g* :b *bg-b*))
720
721 ;; ── Trails ──
722 (maphash (lambda (trail-key val)
723 (let* ((note-name (car trail-key))
724 (oct (cdr trail-key))
725 (rgb (note-color-rgb note-name))
726 (note-idx (or (position note-name *chromatic* :test #'string=) 0))
727 (semi (+ (* (- oct 1) 12) note-idx))
728 (total-semitones (* 9 12))
729 (bar-h (max 2 (floor (- *np-sh* 30) total-semitones)))
730 (bar-y (+ 14 (floor (* semi (- *np-sh* 30)) total-semitones)))
731 (bar-w (max 1 (floor (* val *np-sw*))))
732 (bar-x (floor (- *np-sw* bar-w) 2))
733 (alpha (max 1 (min 255 (floor (* val 200))))))
734 (graph-ink *np-graph* (make-color :r (first rgb)
735 :g (second rgb)
736 :b (third rgb)
737 :a alpha))
738 (graph-box *np-graph* bar-x bar-y bar-w bar-h)))
739 *trails*)
740
741 ;; ── Active note bars ──
742 (maphash (lambda (code note-info)
743 (declare (ignore code))
744 (let* ((note-name (car note-info))
745 (oct (cdr note-info))
746 (rgb (note-color-rgb note-name))
747 (note-idx (or (position note-name *chromatic* :test #'string=) 0))
748 (semi (+ (* (- oct 1) 12) note-idx))
749 (total-semitones (* 9 12))
750 (bar-h (max 2 (floor (- *np-sh* 30) total-semitones)))
751 (bar-y (+ 14 (floor (* semi (- *np-sh* 30)) total-semitones))))
752 (graph-ink *np-graph* (make-color :r (min 255 (+ (first rgb) 40))
753 :g (min 255 (+ (second rgb) 40))
754 :b (min 255 (+ (third rgb) 40))
755 :a 220))
756 (graph-box *np-graph* 0 bar-y *np-sw* bar-h)))
757 *active-notes*)
758
759 ;; ── Metronome pendulum ──
760 (when *metronome-on*
761 (let* ((cx (floor *np-sw* 2))
762 (cy (- *np-sh* 24))
763 (arm-len (min 20 (floor *np-sh* 8)))
764 (bx (+ cx (floor (* *metronome-phase* arm-len))))
765 (bright (floor (* *metronome-flash* 255))))
766 ;; Arm line
767 (graph-ink *np-graph* (make-color :r 180 :g 180 :b 180 :a 120))
768 (graph-line *np-graph* cx cy bx (- cy arm-len))
769 ;; Bob
770 (graph-ink *np-graph* (make-color :r (min 255 (+ 180 bright))
771 :g (min 255 (+ 100 bright))
772 :b 60 :a 220))
773 (graph-circle *np-graph* bx (- cy arm-len) 3)))
774
775 ;; ── Wave type indicators (bottom bar) ──
776 (let* ((bar-y (- *np-sh* 14))
777 (btn-w (max 12 (floor *np-sw* 6)))
778 (gap 2)
779 (total-w (+ (* 5 btn-w) (* 4 gap)))
780 (start-x (floor (- *np-sw* total-w) 2)))
781 (dotimes (i 5)
782 (let* ((bx (+ start-x (* i (+ btn-w gap))))
783 (selected (= i *wave-index*))
784 (col (if selected
785 (make-color :r 255 :g 255 :b 255 :a 200)
786 (make-color :r 100 :g 100 :b 110 :a 140))))
787 ;; Button background
788 (if selected
789 (progn
790 (graph-ink *np-graph* (make-color :r 60 :g 50 :b 80 :a 200))
791 (graph-box *np-graph* bx bar-y btn-w 12))
792 (progn
793 (graph-ink *np-graph* (make-color :r 30 :g 28 :b 35 :a 150))
794 (graph-box *np-graph* bx bar-y btn-w 12)))
795 ;; Wave name (abbreviated to 3 chars)
796 (graph-ink *np-graph* col)
797 (let ((abbr (subseq (aref *wave-names* i) 0 (min 3 (length (aref *wave-names* i))))))
798 (font-draw *np-graph* abbr
799 (+ bx (floor (- btn-w (* (length abbr) 6)) 2))
800 (+ bar-y 2))))))
801
802 ;; ── Status text ──
803 ;; Top-left: piece name + mode indicators
804 (graph-ink *np-graph* (make-color :r 100 :g 100 :b 110 :a 150))
805 (font-draw *np-graph* "notepat" 3 3)
806
807 ;; Quick mode indicator
808 (when *quick-mode*
809 (graph-ink *np-graph* (make-color :r 255 :g 200 :b 50 :a 200))
810 (font-draw *np-graph* "Q" (+ 3 (* 8 6)) 3))
811
812 ;; Handle (top-left, after piece name)
813 (when *boot-handle*
814 (let ((htxt (format nil "@~A" *boot-handle*)))
815 (graph-ink *np-graph* (make-color :r 80 :g 255 :b 140 :a 140))
816 (font-draw *np-graph* htxt (+ 3 (* (if *quick-mode* 10 8) 6)) 3)))
817
818 ;; Octave (top-left, below title)
819 (graph-ink *np-graph* (make-color :r 160 :g 160 :b 170 :a 180))
820 (font-draw *np-graph* (format nil "OCT ~D" *octave*) 3 14)
821
822 ;; Metronome BPM (if on)
823 (when *metronome-on*
824 (graph-ink *np-graph* (make-color :r 180 :g 140 :b 60 :a 200))
825 (font-draw *np-graph* (format nil "~DBPM" *metronome-bpm*)
826 (+ 3 (* 7 6)) 14))
827
828 ;; FPS (top-right)
829 (let ((fps-txt (format nil "~D" *fps-display*)))
830 (graph-ink *np-graph* (make-color :r 80 :g 80 :b 90 :a 120))
831 (font-draw *np-graph* fps-txt (- *np-sw* (* (length fps-txt) 6) 3) 3))
832
833 ;; Voice count (top-right, below FPS)
834 (let ((vc (hash-table-count *active-voices*)))
835 (when (> vc 0)
836 (let ((txt (format nil "~Dv" vc)))
837 (graph-ink *np-graph* (make-color :r 200 :g 200 :b 200 :a 180))
838 (font-draw *np-graph* txt (- *np-sw* (* (length txt) 6) 3) 14))))
839
840 ;; IP + Swank (top center)
841 (when (> (length *ip-address*) 0)
842 (let ((txt (format nil "~A:4005" *ip-address*)))
843 (graph-ink *np-graph* (make-color :r 60 :g 180 :b 60 :a 160))
844 (font-draw *np-graph* txt (- (floor *np-sw* 2) (floor (font-measure txt) 2)) 3)))
845
846 ;; Refresh IP every ~5 seconds
847 (when (zerop (mod *np-frame* 300)) (refresh-ip))
848
849 ;; LISP tag (top right) — always visible in CL variant
850 (when (string= *build-variant* "cl")
851 (graph-ink *np-graph* (make-color :r 255 :g 200 :b 80 :a 160))
852 (font-draw *np-graph* "LISP" (- *np-sw* (font-measure "LISP") 4) 3))
853
854 ;; ── Present ──
855 (ac-native.drm:drm-present *np-display* *np-screen* *np-scale*)
856 (frame-sync-60fps))
857
858 ;; ── Cleanup ──
859 (when *np-audio* (ignore-errors (kill-all-voices *np-audio*)))
860 (when *np-audio* (ignore-errors (audio-destroy *np-audio*)))
861 (when *np-input* (ignore-errors (ac-native.input:input-destroy *np-input*)))
862 (when *np-screen* (ignore-errors (fb-destroy *np-screen*)))
863 (ignore-errors (ac-native.drm:drm-destroy *np-display*))
864 (format *error-output* "[notepat] shutdown~%")
865 (force-output *error-output*)))