emacs-elpa-diffs
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[elpa] externals/midi-kbd d23eea4 1/2: Add packages/midi-kbd/


From: Stefan Monnier
Subject: [elpa] externals/midi-kbd d23eea4 1/2: Add packages/midi-kbd/
Date: Sat, 28 Nov 2020 23:25:14 -0500 (EST)

branch: externals/midi-kbd
commit d23eea421486cf94e0282ac0cc43fd9700174c77
Author: David Kastrup <dak@gnu.org>
Commit: David Kastrup <dak@gnu.org>

    Add packages/midi-kbd/
    
    This introduces the package midi-kbd version 0.2 for converting raw MIDI
    input into Emacs events.
---
 midi-kbd.el | 283 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 283 insertions(+)

diff --git a/midi-kbd.el b/midi-kbd.el
new file mode 100644
index 0000000..5bdcc55
--- /dev/null
+++ b/midi-kbd.el
@@ -0,0 +1,283 @@
+;;; midi-kbd.el --- Create keyboard events from Midi input  -*- 
lexical-binding: t; -*-
+
+;; Copyright (C) 2015 Free Software Foundation, Inc.
+
+;; Author: David Kastrup <dak@gnu.org>
+;; Keywords: convenience, hardware, multimedia
+;; Version: 0.2
+;; Maintainer: David Kastrup <dak@gnu.org>
+;; Package-Requires: ((emacs "25"))
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Entry point of this package is M-x midikbd-open RET
+;;
+;; It opens a raw ALSA midi device (see its documentation for how to
+;; deal with non-raw devices) and feeds MIDI note-on and note-off
+;; events into the Emacs input queue associated with the terminal from
+;; which midikbd-open has been called.  Macro recording and replay is
+;; possible.  The interpretation of such events is left to
+;; applications establishing appropriate key bindings.
+;;
+;; Since macro recording and replay makes it very desirable to have
+;; every generated event be interpretable standalone rather than split
+;; into several Emacs events, every MIDI event is encoded into one
+;; mouse-like event similar to <Ch1 C_4>.  Consequently, the following
+;; functions are applicable to such events:
+;;
+;; (event-start EVENT) returns the down event part
+;; (event-end EVENT) returns the up event part
+;;
+;; The up event is only available with bindings of <Ch1 up-C-4> and
+;; similar, whereas the down event is available for all bindings.
+;;
+;; up/down event parts may be further split with
+;;
+;; (posn-area EV) returns a channel symbol Ch1..Ch16
+;;
+;; (posn-x-y EV) returns numeric values 0..127 for pitch and velocity
+;;
+;; (posn-timestamp EV) returns a millisecond time value that will wrap
+;; around when reaching most-positive-fixnum, about every 12 days on a
+;; 32bit system.
+;;
+;; Note events (omitting the channel modifier) are
+;; <C_-1> <Csharp_-1> ... <G_9>
+;;
+;; Since Midi does not encode enharmonics, there are no *flat_* key
+;; names: it is the job of the key bindings to give a higher level
+;; interpretation to the basic pitch.
+
+;;; Code:
+
+
+(defconst midikbd-notenames
+  (vconcat
+   (cl-loop for i from 0 to 127
+           collect (intern
+                    (format "%s_%d"
+                            (aref ["C" "Csharp" "D" "Dsharp" "E" "F"
+                                   "Fsharp" "G" "Gsharp" "A" "Asharp" "B"]
+                                  (mod i 12))
+                            (1- (/ i 12)))))))
+
+;; Necessary to allow bindings to <Ch1 C_4> without splitting events
+(cl-loop for key across midikbd-notenames do
+        (put key 'event-kind 'mouse-click))
+
+;; We have `midikbd-notenames' for looking up the basic note name
+;; events, `midikbd-upnames' for the keyrelease events, and
+;; `midikbd-downnames' for the keypress events.  Those will, for now,
+;; produce the likes of `C_-1', `up-C_-1', and `C_-1': we don't
+;; actually use `down-C_-1' since the down-event is the principally
+;; important one most likely to be bound to keys.
+
+(defconst midikbd-downnames midikbd-notenames)
+
+(defconst midikbd-upnames
+  (vconcat
+   (cl-loop for i across midikbd-notenames
+           collect
+           (intern (concat "up-" (symbol-name i))))))
+
+;; Emacs can deal with up-events like with down-events since the patch
+;; in <URL:http://debbugs.gnu.org/cgi/bugreport.cgi?bug=19746> has
+;; been committed to Emacs.
+;;
+;; Older versions will erupt in violence when forced to deal with an
+;; uncached "up-" event, so we need to put the full cache in place
+;; ourselves.  We do this only if we find Emacs unable to identify
+;; up-events.
+
+;; Calling event-modifiers may poison the cache for up-C_-1 but since
+;; we overwrite it first thing afterwards, this is not really an
+;; issue.
+
+(unless (event-modifiers 'up-C_-1)
+  (cl-loop for key across midikbd-upnames for base across midikbd-notenames
+          do
+          (put key 'event-symbol-element-mask (list base 1))
+          (put key 'event-symbol-elements (list base 'up))
+          (let ((modc (get base 'modifier-cache)))
+            (unless (assq 1 modc)
+              (put base 'modifier-cache (cons (cons 1 key) modc))))))
+
+
+(defconst midikbd-channelnames
+  [Ch1 Ch2 Ch3 Ch4 Ch5 Ch6 Ch7 Ch8
+       Ch9 Ch10 Ch11 Ch12 Ch13 Ch14 Ch15 Ch16])
+
+;; CCL programs used in coding systems apparently don't save registers
+;; across suspension so we don't use a coding system.  Instead our CCL
+;; program is run using ccl-execute-on-string in the filter routine.
+;; That allows us to interpret all _completed_ Midi commands without
+;; getting confused, and it also gives us a well-defined internal
+;; state (namely one for every call of midikbd-filter-create).
+
+;; Decoding Midi is a positive nuisance because of "running status":
+;; if a Midi command byte is the same as the last one, it can be
+;; omitted and just the data sent.
+;; So we keep the current command in r0, the currently read byte in r1,
+;; the channel in r6.
+
+(define-ccl-program midikbd-decoder
+  '(2
+    (loop
+     (loop
+      ;; central message receiver loop here.
+      ;; When it exits, the command to deal with is in r0
+      ;; Any arguments are in r1 and r2
+      ;; r3 contains: 0 if no arguments are accepted
+      ;;              1 if 1 argument can be accepted
+      ;;              2 if 2 arguments can be accepted
+      ;;              3 if the first of two arguments has been accepted
+      ;; Arguments are read into r1 and r2.
+      ;; r4 contains the current running status byte if any.
+      (read-if (r0 < #x80)
+              (branch r3
+                      (repeat)
+                      ((r1 = r0) (r0 = r4) (break))
+                      ((r1 = r0) (r3 = 3) (repeat))
+                      ((r2 = r0) (r3 = 2) (r0 = r4) (break))))
+      (if (r0 >= #xf8) ; real time message
+         (break))
+      (if (r0 < #xf0) ; channel command
+         ((r4 = r0)
+          (if ((r0 & #xe0) == #xc0)
+              ;; program change and channel pressure take only 1 argument
+              (r3 = 1)
+            (r3 = 2))
+          (repeat)))
+      ;; system common message, we swallow those for now
+      (r3 = 0)
+      (repeat))
+     (if ((r0 & #xf0) == #x90)
+        (if (r2 == 0)              ; Some Midi devices use velocity 0
+                                       ; for switching notes off,
+                                       ; so translate into note-off
+                                       ; and fall through
+            (r0 -= #x10)
+          ((r0 &= #xf)
+           (write 0)
+           (write r0 r1 r2)
+           (repeat))))
+     (if ((r0 & #xf0) == #x80)
+        ((r0 &= #xf)
+         (write 1)
+         (write r0 r1 r2)
+         (repeat)))
+     (repeat))))
+
+(defun midikbd-get-ts-lessp (pivot)
+  "Return a comparison operator for timestamps close to PIVOT.
+
+Timestamps are just a millisecond count that wraps around
+eventually.  To compare two timestamps TS1 and TS2, one can
+generally just look at the sign of their difference.  However,
+this relation is not really transitive when given input spanning
+more than half of the given number range (should only happen in
+degenerate cases since the overall range spans several days).
+
+Sort algorithms may require transitivity in order to complete, so
+this routine creates a transitive comparison operator when given
+a \"pivot\" from within the sorted range."
+  (lambda (ts1 ts2)
+    (< (- ts1 pivot) (- ts2 pivot))))
+
+(defun midikbd-filter-create ()
+  "Create one Midi process filter keeping state across calls."
+  (let* ((state (make-vector 9 nil))
+        (keypress (make-vector 2048 nil))
+        (param-len [3 3])
+        (hooks (vector
+                (lambda (ts ch pitch velocity)
+                  (let ((res
+                         (list (aref midikbd-downnames pitch)
+                               (list nil
+                                     (aref midikbd-channelnames ch)
+                                     (cons pitch velocity)
+                                     ts))))
+                    (aset keypress (+ (* ch 128) pitch) res)
+                    (list res)))
+                (lambda (ts ch pitch velocity)
+                  (let* ((idx (+ (* ch 128) pitch))
+                         (oldpress (prog1 (aref keypress idx)
+                                     (aset keypress idx nil))))
+                    (and oldpress
+                         (list
+                          (list (aref midikbd-upnames pitch)
+                                (cadr oldpress)
+                                (list nil
+                                      (aref midikbd-channelnames ch)
+                                      (cons pitch velocity)
+                                      ts)))))))))
+    (lambda (_process string)
+      (let* ((ct (current-time))
+            (ts (+ (* (nth 0 ct) 65536000)
+                   (* (nth 1 ct) 1000)
+                   (/ (nth 2 ct) 1000)))
+            (str (ccl-execute-on-string 'midikbd-decoder
+                                        state string t t)))
+       (setq unread-command-events
+             (append unread-command-events
+                     (cl-loop with i = 0 while (< i (length str))
+                              nconc
+                              (let* ((code (aref str i))
+                                     (beg (1+ i)))
+                                (setq i (+ beg (aref param-len code)))
+                                (apply (aref hooks code)
+                                       ts
+                                       (append (substring str beg i)
+                                               nil))))))))))
+
+(defcustom midikbd-default-device
+  "/dev/snd/midiC1D0"
+  "Default MIDI raw device for midikbd."
+  :type '(file)
+  :group 'midi-kbd
+  :package-version '(midi-kbd . "0.2"))
+
+;;;###autoload
+(defun midikbd-open (file)
+  "Open the raw Midi device FILE as a source for Midi input.
+This should be an ALSA device like \"/dev/snd/midiC1D0\".  If your
+Midi producing device is a software Midi device, you might need to
+call
+
+    sudo modprobe snd-virmidi
+
+in order to have some virtual ALSA ports available as such raw Midi
+devices."
+  (interactive (list (read-file-name "Midi device: "
+                                    (file-name-directory 
midikbd-default-device)
+                                    (file-name-nondirectory 
midikbd-default-device)
+                                    t nil
+                                    #'file-readable-p)))
+  (let* ((file (expand-file-name file
+                                (file-name-directory midikbd-default-device)))
+        (buffer (get-buffer-create (concat " *Midi process " file " *")))
+        (oldproc (get-buffer-process buffer)))
+    (if (processp oldproc) (delete-process oldproc))
+    (make-serial-process :port file
+                        :speed nil
+                        :buffer buffer
+                        :coding 'raw-text
+                        :filter (midikbd-filter-create)
+                        :sentinel #'ignore
+                        :noquery t)))
+
+(provide 'midi-kbd)
+;;; midi-kbd.el ends here



reply via email to

[Prev in Thread] Current Thread [Next in Thread]