-
Notifications
You must be signed in to change notification settings - Fork 11
/
Copy pathbackend.lisp
353 lines (273 loc) · 17.6 KB
/
backend.lisp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
;;;; backend.lisp - general functionality to implement cl-patterns sound server backends.
;;; specific backends are defined in files in the src/backends/ directory.
(in-package #:cl-patterns)
;;; helpers
;; FIX: move these?
(defun task-nodes (task &optional backend)
"Get the list of nodes for TASK for the specified backend. If BACKEND is nil, get all of the resources for TASK regardless of backend."
(with-slots (backend-resources) task
(if backend
(remove-if-not (curry #'backend-node-p backend) backend-resources)
backend-resources)))
(defun (setf task-nodes) (value task &optional backend)
"Set the list of nodes for TASK for the specified backend. If BACKEND is nil, set the full backend-resources slot for the task."
(with-slots (backend-resources) task
(setf backend-resources (if backend
(append (ensure-list value)
(remove-if (curry #'backend-node-p backend) backend-resources))
(ensure-list value)))))
(defun event-backends (event)
"Get a list of backends that EVENT might be playable on, either via the event's :backend key or via the `enabled-backends'."
(or (mapcar #'find-backend (ensure-list (or (event-value event :backend)
(event-value event :backends))))
(enabled-backends)))
;;; backend management
(eval-when (:compile-toplevel :load-toplevel :execute)
(defclass backend ()
((name :initarg :name :accessor backend-name :type string-designator :documentation "The name of the backend instance.")
(enabled-p :initarg :enabled-p :initform t :accessor backend-enabled-p :type boolean :documentation "Whether this backend instance is currently enabled. Events being played will only be sent to enabled and running backends.")
(started-p :initarg :started-p :initform nil :accessor backend-started-p :type boolean :documentation "Whether the backend is current enabled and running.")
(input-processors :initarg :input-processors :initform nil :accessor backend-input-processors :type list :documentation "List of functions that process incoming events. Similar to `*post-pattern-output-processors*' but per-backend.")
(metadata :initarg :metadata :initform nil :accessor backend-metadata :type list :documentation "Additional metadata associated with the backend instance."))
(:documentation "Abstract superclass for backends.")))
(defmethod initialize-instance :after ((backend backend) &key &allow-other-keys)
(pushnew backend *backends*))
(eval-when (:compile-toplevel :load-toplevel :execute) ; needed for the following dolist form.
(closer-mop:ensure-finalized (find-class 'backend)))
(dolist (sym (list 'backend-name 'backend-enabled-p 'backend-started-p 'backend-input-processors 'backend-metadata))
(setf (documentation sym 'function) (documentation (find-class-slot 'backend :accessor sym) t)))
(defun all-backend-types ()
"Get a list of names of all defined backend types. A backend type is any class that inherits from `backend'.
See also: `all-backends', `enabled-backends'"
(mapcar #'class-name (subclasses-of 'backend)))
(defvar *backends* nil
"List of loaded and active cl-patterns backends. This variable generally shouldn't need to be modified by the user; instead register backends by loading the relevant cl-patterns sub-system, then use `make-backend' and/or `backend-start' to make and activate a backend.
See also: `all-backends', `all-backend-types', `make-backend', `backend-start', `backend-stop'")
(defun all-backends (&key (enabled-p nil enabled-p-provided-p) (started-p nil started-p-provided-p))
"Get a list of all backends made with `make-backend' or `backend-start'. ENABLED-P and STARTED-P can be used to limit the results only to backends with matching status.
See also: `all-backend-types', `enabled-backends'"
(loop :for backend :in *backends*
:if (and (if enabled-p-provided-p
(eql enabled-p (backend-enabled-p backend))
t)
(if started-p-provided-p
(eql started-p (backend-started-p backend))
t))
:collect backend))
(defun backend-p (object)
"Test whether OBJECT is a `backend'.
See also: `find-backend', `all-backends'"
(typep object 'backend))
(defun find-backend (backend &rest args &key enabled-p started-p)
"Find a registered backend whose name or type matches BACKEND. With ENABLED-P, only search through currently enabled backends; with STARTED-P, only backends that have been started.
See also: `all-backends', `enabled-backends'"
(declare (ignore enabled-p started-p))
(when (typep backend 'backend)
(return-from find-backend backend))
(find-if (fn (or (string-equal backend (backend-name _))
(string-equal backend (class-name (class-of _)))))
(apply #'all-backends args)))
(defgeneric make-backend (backend &rest rest &key &allow-other-keys)
(:documentation "Make a `backend' of the specified type and return it.
See also: `backend-start'"))
(defmethod make-backend (backend &rest rest &key &allow-other-keys)
(if-let ((found-backend (find backend (all-backend-types) :test #'string-equal)))
(apply #'make-instance found-backend rest)
(apply #'no-applicable-method #'make-backend backend rest)))
(defmethod backend-started-p ((backend symbol))
(backend-started-p (find-backend backend)))
(defmethod backend-started-p ((null null))
nil)
(defgeneric backend-start (backend &rest args &key &allow-other-keys)
(:documentation "Start BACKEND so it is ready to handle events and return the backend object. If BACKEND is the name of a backend rather than a `backend' instance, first make an instance of the backend as if `make-backend' was called, then call `backend-start' on that.
See also: `backend-stop', `backend-enabled-p', `make-backend'"))
(defmethod backend-start ((name symbol) &rest args &key &allow-other-keys)
(let ((backend (or (find-backend name)
(apply #'make-backend name args))))
(if (backend-started-p backend)
(progn
(warn "Backend ~S already started" backend)
(return-from backend-start backend))
(apply #'backend-start backend :allow-other-keys t args))))
(defmethod backend-start ((backend backend) &key &allow-other-keys)
nil)
(defmethod backend-start :around ((backend backend) &key &allow-other-keys)
(call-next-method)
(setf (backend-started-p backend) t)
backend)
(defgeneric backend-stop (backend)
(:documentation "Stop BACKEND's server if it is running and return the affected backend.
See also: `backend-start', `backend-enabled-p'"))
(defmethod backend-stop (backend)
nil)
(defmethod backend-stop ((backend-name symbol))
(if-let ((backend (find-backend backend-name :started-p t)))
(progn (backend-stop backend)
backend)
(error "Could not find a started backend of name or type ~S" backend-name)))
(defmethod backend-stop :around (backend)
(let ((backend (call-next-method)))
(setf (backend-started-p backend) nil)
backend))
;; (defgeneric backend-handles-event-p (backend event) ; FIX: is this needed?
;; (:documentation "True if BACKEND is currently available to handle EVENT."))
;; (defmethod backend-handles-event-p (backend event)
;; )
(defun enabled-backends () ; FIX: remove this?
"Get a list of all enabled backends.
See also: `all-backends', `backend-enabled-p'"
(remove-if-not #'backend-enabled-p (all-backends)))
(defgeneric backend-responds-p (backend event)
(:documentation "True if BACKEND should respond to EVENT."))
(defmethod backend-responds-p (backend event)
t)
(defmethod backend-responds-p (event (backend backend))
(backend-responds-p backend event))
(defgeneric backend-play-event (backend event task)
(:documentation "Play ITEM on the sound server specified by BACKEND. TASK is the task that triggered ITEM to be played. Typically a backend should not need to define a specialized method for this generic if it already provides methods for the following:
- `backend-instrument-controls'
- `backend-node-p'
- `backend-timestamps-for-event'
- `backend-proxys-node'
- `backend-control-node-at'
It's suggested to define methods for `backend-convert-object' if the backend requires objects to be converted to another representation before being used as an instrument's parameters. Additionally, methods for `play', `launch', `stop', and `end' may be convenient to define for the backend's node class.
See also: `backend-task-removed'"))
;; FIX: move all of the node stuff out of backend? or is it possible that some backends would know voice allocation better than we do?
(defmethod backend-play-event (backend event task)
(let ((type (event-value event :type))
(instrument (instrument event)))
(case type
(:rest
nil)
(:tempo
(backend-tempo-change-at backend
(task-clock task)
(car (backend-timestamps-for-event backend event task))))
(t
(when (and (not (position type (list :rest :tempo)))
(or (backend-instrument-controls backend instrument)
(backend-node-p backend instrument)))
(let ((time (backend-timestamps-for-event backend event task))
(params (backend-instrument-args-list backend instrument event)))
(if (or (eql type :mono)
(backend-node-p backend instrument))
(let ((node (backend-control-node-at backend
(first time)
(let ((nodes (task-nodes task backend)))
(cond (nodes
(car nodes))
((backend-node-p backend instrument)
(or (backend-proxys-node backend instrument)
instrument))
(t instrument)))
params)))
(unless (or (backend-node-p backend instrument)
(not (backend-instrument-has-gate-p backend instrument)))
(if (< (legato event) 1)
(progn
(backend-control-node-at backend (second time) node (list :gate 0))
(setf (task-nodes task backend) nil))
(setf (task-nodes task backend) (list node)))))
(let ((node (backend-control-node-at backend (first time) instrument params)))
;; FIX: should add NODE to the task's backend-resources slot, then free it when it stops
(when (backend-instrument-has-gate-p backend instrument)
(backend-control-node-at backend (second time) node (list :gate 0)))))))))))
(defmethod backend-play-event :around (backend event task)
(call-next-method backend
(loop :with result := event
:for proc :in (backend-input-processors backend)
:do (setf result (funcall proc result (task-item task)))
:finally (return result))
task))
(defgeneric backend-tempo-change-at (backend clock timestamp)
(:documentation "Set the backend's tempo to NEW-TEMPO at the timestamp provided."))
(defmethod backend-tempo-change-at (backend clock timestamp)
nil)
(defgeneric backend-task-added (backend task) ; FIX: when created, backend objects should be sent all current tasks
(:documentation "Called when TASK is added to the clock so BACKEND can prepare any related state.
See also: `backend-task-removed'"))
(defmethod backend-task-added (backend task)
nil)
(defgeneric backend-task-removed (backend task)
(:documentation "Called when TASK is removed from the clock so BACKEND can free any associated nodes. Typically a backend shouldn't need to define a method for this generic if it already defines methods for the events listed in the docstring for `backend-play-event'.
See also: `backend-play-event'"))
(defmethod backend-task-removed (backend task) ; FIX: this code should probably be run by the clock rather than a backend method since it should apply to all backends (maybe?)
(let ((item (slot-value task 'item))
(nodes (task-nodes task backend)))
(if (event-p item)
(mapc #'stop nodes) ; FIX: this doesn't work because the preview synth doesn't have a gate argument, and non-gated synths aren't kept in task's backend-resources slot.
(let ((last-output (last-output item)))
(dolist (node nodes)
(backend-control-node-at backend
(cadr (backend-timestamps-for-event
backend
(event-with-raw-timing (combine-events last-output (event :legato 1))
task)
task))
node
(list :gate 0)))))
(when-let ((cleanup (and (slot-exists-p item 'cleanup)
(slot-value item 'cleanup))))
(mapc 'funcall cleanup)))
(setf (task-nodes task backend) nil))
(defgeneric backend-instrument-controls (backend instrument)
(:documentation "Get the list of names of controls for INSTRUMENT in BACKEND."))
(defun backend-instrument-has-gate-p (backend instrument)
"Whether or not SYNTH in BACKEND has a gate control (i.e. whether it needs to be manually released or if it acts as a \"one-shot\")."
(position :gate (backend-instrument-controls backend instrument) :test 'string-equal))
(defgeneric backend-instrument-args-list (backend instrument event)
(:documentation "Generate a plist of parameters for INSTRUMENT based off of its controls, taking values from EVENT. Unlike `event-plist', this function doesn't include event keys that aren't also one of the synth's arguments."))
(defmethod backend-instrument-args-list (backend instrument event)
(if-let ((controls (backend-instrument-controls backend instrument)))
(let ((instrument-params (remove-if (lambda (arg) ; for parameters unspecified by the event, we fall back to the instrument's defaults, NOT the event's...
(unless (string-equal arg :sustain) ; ...with the exception of sustain, which the instrument should always get.
(multiple-value-bind (value key) (event-value event arg)
(declare (ignore value))
(eql key t))))
(append controls (list :group :to :id))))) ; FIX: this is for the supercollider backend; genericize this
;; get the value of each of the instrument's arguments from the event...
(loop :for param :in instrument-params
:for sparam := (make-keyword (string-upcase param))
:for val := (backend-convert-object backend (event-value event sparam) sparam)
:if (or val (eql :gate sparam))
:append (list (if (eql :group sparam) ; :group is an alias for :to
:to
sparam)
(if (eql :gate sparam) 1 val))))
(copy-list (event-plist event)))) ; if we don't have data for the instrument, all we can do is return the plist for the event and hope for the best.
(defgeneric backend-all-instruments (backend)
(:documentation "Get a list of the names of all defined synths (synthdefs, dsps, etc) for the specified backend.
See also: `backend-all-nodes'"))
(defmethod backend-all-instruments (backend)
nil)
(defmethod backend-all-instruments ((backend symbol))
(backend-all-instruments (find-backend backend)))
(defgeneric backend-all-nodes (backend)
(:documentation "Get a list of all active nodes for the specified backend.
See also: `backend-all-instruments', `backend-node-p', `backend-panic'"))
(defmethod backend-all-nodes (backend)
nil)
(defgeneric backend-node-p (backend object)
(:documentation "True if OBJECT is a node for the specified backend.
See also: `backend-all-nodes'"))
(defgeneric backend-panic (backend)
(:documentation "\"Panic\" the backend, i.e. stop all of its nodes immediately.
See also: `backend-all-nodes'"))
(defmethod backend-panic (backend)
(stop (backend-all-nodes backend)))
(defgeneric backend-timestamps-for-event (backend event task)
(:documentation "Get a list containing timestamps for the start and end of EVENT's note, in the format that BACKEND expects for its scheduling function.
See also: `backend-control-node-at'"))
(defgeneric backend-proxys-node (backend id)
(:documentation "Get the current node object on BACKEND for the proxy with the specified ID."))
(defgeneric backend-control-node-at (backend time node params)
(:documentation "At TIME, set NODE's parameters to PARAMS. If NODE is an instrument name, launch a node with those parameters at the specified time instead. This function should return the node object."))
(defgeneric backend-convert-object (backend object key)
(:documentation "Convert OBJECT to a value that BACKEND can understand. For example, the SuperCollider backend requires that any `cl-collider::buffer' objects are converted to their bufnum."))
(defmethod backend-convert-object (backend object key)
(declare (ignore key))
object)
(defmethod backend-convert-object (backend (symbol symbol) key)
(declare (ignore key backend))
(when symbol
(find-object-by-id symbol)))