-
-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathvoice.lisp
287 lines (241 loc) · 11.4 KB
/
voice.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
(in-package #:org.shirakumo.fraf.harmony)
(defun ensure-effect-segment (segment-ish channels)
(flet ((channel-mismatch (segment)
(error "Cannot connect~% ~a~%as an effect segment as it does not comply with the required channel count of ~d"
segment channels)))
(etypecase segment-ish
(segment
(destructuring-bind (&key min-inputs max-inputs &allow-other-keys) (mixed:info segment-ish)
(unless (<= min-inputs channels max-inputs)
(channel-mismatch segment-ish))
segment-ish))
((or symbol class cons)
(let* ((init (if (listp segment-ish) segment-ish (list segment-ish)))
(proto (apply #'make-instance init)))
(destructuring-bind (&key min-inputs max-inputs &allow-other-keys) (mixed:info proto)
(cond ((<= min-inputs channels max-inputs)
proto)
((= min-inputs max-inputs 1)
(let ((bundle (mixed:make-bundle channels)))
(when (getf (rest init) :name)
(setf (slot-value bundle 'name) (getf (rest init) :name)))
(setf (aref (mixed:segments bundle) 0) proto)
(loop for i from 1 below channels
do (setf (aref (mixed:segments bundle) i) (apply #'make-instance init)))
bundle))
(T
(mixed:free proto)
(channel-mismatch proto)))))))))
(defclass voice (mixed:chain)
())
(defmethod print-object ((voice voice) stream)
(print-unreadable-object (voice stream :type T :identity (null (name voice)))
(format stream "~@[~a ~]" (name voice))
(ignore-errors
(let ((source (source voice)))
(cond ((mixed:done-p source)
(write-string "DONE" stream))
((null (mixed:frame-count source))
(write-string "STREAM" stream))
(T
(format stream "~2d%" (floor (* (/ (mixed:frame-position source) (mixed:frame-count source)) 100)))))))))
(defgeneric make-source-for (source &rest initargs)
(:method ((source pathname) &rest initargs)
(if (pathname-type source)
(apply #'make-source-for-path-type source (intern (string-upcase (pathname-type source)) "KEYWORD") initargs)
(error "Pathname has no type:~% ~a" source)))
(:method ((source source) &rest initargs &key on-end)
(declare (ignore initargs))
;; FIXME: this is not right since we discard the unpacker...
(when on-end (setf (on-end source) on-end))
source))
(defgeneric make-source-for-path-type (pathname type &rest initargs)
(:method (source type &rest initargs)
(macrolet ((maybe-make-drain (package system &optional (name 'source))
`(apply #'make-instance
(handler-bind (#+quicklisp
(error (lambda (e)
(declare (ignore e))
(ql:quickload ,(string system))
(invoke-restart 'retry))))
(lazy-symbol ,package ,name))
:file source initargs)))
(ecase type ;; static deferral. Not great, but can't do it otherwise with ASDF.
(:mp3 (maybe-make-drain org.shirakumo.fraf.mixed.mpg123 cl-mixed-mpg123))
(:wav (maybe-make-drain org.shirakumo.fraf.mixed.wav cl-mixed-wav in-memory-source))
(:flac (maybe-make-drain org.shirakumo.fraf.mixed.flac cl-mixed-flac))
(:ogg (maybe-make-drain org.shirakumo.fraf.mixed.vorbis cl-mixed-vorbis))
(:oga (maybe-make-drain org.shirakumo.fraf.mixed.vorbis cl-mixed-vorbis))
(:opus (maybe-make-drain org.shirakumo.fraf.mixed.opus cl-mixed-opus))
(:qoa (maybe-make-drain org.shirakumo.fraf.mixed.qoa cl-mixed-qoa))
((:mptm :mod :s3m :xm :it :669 :amf :ams :c67 :dbm :digi :dmf :dsm :dtm :far
:imf :ice :j2b :m15 :mdl :med :mms :mt2 :mtm :nst :okt :plm :psm :pt36 :ptm
:sfx :sfx2 :st26 :stk :stm :stp :ult :wow :gdm :mo3 :oxm :umx :xpk :ppm :mmcmp)
(maybe-make-drain org.shirakumo.fraf.mixed.mpt cl-mixed-mpt))))))
(defmethod initialize-instance :after ((voice voice) &rest args &key source effects channels (on-end :free) &allow-other-keys)
(flet ((free (_) (declare (ignore _))
(with-server (*server* :synchronize NIL)
(mixed:free voice)))
(disconnect (_) (declare (ignore _))
;; NOTE: Pretty sure these WITH-SERVERs are unnecessary, as ON-END should only be called from
;; within the mixing thread.
(with-server (*server* :synchronize NIL)
(disconnect voice T)
(mixed:withdraw voice T)
(mixed:seek voice 0)))
(track-end (source)
(track-end voice source))
(call (_) (declare (ignore _))
(funcall on-end voice))
(on-frame-change (seg pos)
(frame-change voice (mixed:frame-position seg) pos)))
(let ((unpacker (allocate-unpacker *server*))
(args (removef args :source :effects :channels :on-end))
(on-end (etypecase on-end
((eql :free) #'free)
((eql :disconnect) #'disconnect)
((eql :call-track-end) #'track-end)
(function #'call))))
(flet ((on-end (arg)
;; KLUDGE: we have to defer the ending until all output samples have been drained.
(if (= 0 (mixed:available-read (mixed:output 0 voice)))
(funcall on-end arg)
(setf (mixed:done-p voice) NIL))))
(mixed:add (apply #'make-source-for source :pack (mixed:pack unpacker) :on-end #'on-end :on-frame-change #'on-frame-change args) voice))
(mixed:add unpacker voice)
(mixed:revalidate unpacker)
(dolist (effect effects)
(let ((outputs (getf (mixed:info (voice-end voice)) :outputs)))
(mixed:add (ensure-effect-segment effect outputs) voice)))
(let ((outputs (length (mixed:outputs (voice-end voice)))))
(when (and channels (/= channels outputs))
(mixed:add (mixed:make-channel-convert :in outputs :out channels) voice))))))
(defmethod mixed:free :before ((voice voice))
(when (< 0 (length (mixed:segments voice)))
(mixed:withdraw voice T)
(when (name voice)
(setf (segment (name voice) *server*) NIL))
(when (and (mixer voice) (mixed:handle (mixer voice)))
(disconnect voice T))))
(defmethod mixed:free :after ((voice voice))
(when (< 0 (length (mixed:segments voice)))
(mixed:free (source voice))
(free-unpacker (mixed:unpacker voice) *server*)
(loop for i from 2 below (length (mixed:segments voice))
for segment = (aref (mixed:segments voice) i)
do (disconnect segment T)
(mixed:free segment))
(setf (fill-pointer (mixed:segments voice)) 0)))
(defmethod mixed:add :before ((segment segment) (voice voice))
(when (< 1 (length (mixed:segments voice)))
(connect (voice-end voice) T segment T)))
(defmethod frame-change ((voice voice) old new)
(declare (ignorable old new))
)
(defmethod track-end ((voice voice) source)
(declare (ignorable source))
)
(defmethod source ((voice voice))
(aref (mixed:segments voice) 0))
(defmethod mixer ((voice voice))
(let ((output (mixed:output 0 voice)))
(when output
(to output))))
(defmethod mixed:unpacker ((voice voice))
(aref (mixed:segments voice) 1))
(defmethod mixed:volume ((voice voice))
(mixed:volume (mixed:unpacker voice)))
(defmethod (setf mixed:volume) (value (voice voice))
(setf (mixed:volume (mixed:unpacker voice)) value))
(defun voice-end (voice)
(aref (mixed:segments voice) (1- (length (mixed:segments voice)))))
(defmethod connect ((from voice) from-loc to to-loc)
(connect (voice-end from) from-loc to to-loc))
(defmethod disconnect ((from voice) from-loc &key (direction :output))
(unless (eq direction :output)
(error "Cannot disconnect voice from input, as it does not have any."))
(disconnect (voice-end from) from-loc :direction :output))
(defmethod repeat ((voice voice))
(repeat (source voice)))
(defmethod (setf repeat) (value (voice voice))
(setf (repeat (source voice)) value))
(defmethod mixed:outputs ((from voice))
(mixed:outputs (voice-end from)))
(defmethod mixed:output (location (from voice))
(mixed:output location (voice-end from)))
(defmethod (setf mixed:output) (value location (from voice))
(setf (mixed:output location (voice-end from)) value))
(defmethod mixed:done-p ((voice voice))
(mixed:done-p (source voice)))
(defmethod (setf mixed:done-p) (value (voice voice))
(setf (mixed:done-p (source voice)) value))
(defmacro with-buffer ((buffer voice) &body body)
`(let ((,buffer (mixed:output 0 ,voice)))
(when ,buffer
,@body)))
(defmethod mixed:min-distance ((voice voice))
(with-buffer (buffer voice)
(mixed:input-min-distance buffer (to buffer))))
(defmethod (setf mixed:min-distance) (min-distance (voice voice))
(with-buffer (buffer voice)
(setf (mixed:input-min-distance buffer (to buffer)) min-distance)))
(defmethod mixed:max-distance ((voice voice))
(with-buffer (buffer voice)
(mixed:input-max-distance buffer (to buffer))))
(defmethod (setf mixed:max-distance) (max-distance (voice voice))
(with-buffer (buffer voice)
(setf (mixed:input-max-distance buffer (to buffer)) max-distance)))
(defmethod mixed:rolloff ((voice voice))
(with-buffer (buffer voice)
(mixed:input-rolloff buffer (to buffer))))
(defmethod (setf mixed:rolloff) (rolloff (voice voice))
(with-buffer (buffer voice)
(setf (mixed:input-rolloff buffer (to buffer)) rolloff)))
(defmethod mixed:location ((voice voice))
(with-buffer (buffer voice)
(mixed:input-location buffer (to buffer))))
(defmethod (setf mixed:location) (location (voice voice))
(with-buffer (buffer voice)
(setf (mixed:input-location buffer (to buffer)) location)))
(defmethod mixed:velocity ((voice voice))
(with-buffer (buffer voice)
(mixed:input-velocity buffer (to buffer))))
(defmethod (setf mixed:velocity) (velocity (voice voice))
(with-buffer (buffer voice)
(setf (mixed:input-velocity buffer (to buffer)) velocity)))
(defmethod mixed:seek-to-frame ((voice voice) frame)
(mixed:seek-to-frame (source voice) frame))
(defmethod mixed:seek ((voice voice) position &rest args)
(apply #'mixed:seek (source voice) position args)
voice)
(defmethod mixed:frame-position ((voice voice))
(mixed:frame-position (source voice)))
(defmethod mixed:frame-count ((voice voice))
(mixed:frame-count (source voice)))
(defmethod mixed:duration ((voice voice))
(mixed:duration (source voice)))
(defmethod mixed:samplerate ((voice voice))
(mixed:samplerate (aref (mixed:segments voice) 1)))
(defmethod active-p ((voice voice))
(not (null (chain voice))))
(defmethod stop ((voice voice))
(when (chain voice)
(with-server (*server* :synchronize NIL)
(disconnect voice T)
(mixed:withdraw voice T)
(clear-buffers voice)))
voice)
(defun clear-buffers (thing)
(etypecase thing
(mixed:chain
(loop for element across (mixed:segments thing)
do (clear-buffers element)))
(mixed:segment
(loop for buffer across (mixed:inputs thing)
do (when buffer (mixed:clear buffer)))
(loop for buffer across (mixed:outputs thing)
do (when buffer (mixed:clear buffer))))
(mixed:buffer
(mixed:clear thing))
(mixed:pack
(mixed:clear thing))))