forked from MusicPlayerDaemon/MPD
/
VisualizationOutputPlugin.cxx
712 lines (645 loc) · 27.9 KB
/
VisualizationOutputPlugin.cxx
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
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright The Music Player Daemon Project
#include "VisualizationOutputPlugin.hxx"
#include "SoundAnalysis.hxx"
#include "SoundInfoCache.hxx"
#include "VisualizationServer.hxx"
#include "Log.hxx"
#include "config/Block.hxx"
#include "event/Call.hxx"
#include "lib/fmt/ThreadIdFormatter.hxx"
#include "output/Interface.hxx"
#include "output/OutputPlugin.hxx"
#include "util/Domain.hxx"
#include <chrono>
namespace Visualization {
/**
* \page vis_out_protocol Visualization Network Protocol
*
* See \ref vis_out "RFC: Visualization Output Plugin" for background.
*
*
* \section vis_out_protocol_timing Timing
*
* In order to deliver sound data to the client at the proper time, the protocol
* needs to take into account:
*
* - network latency: the delta between writing the sound data to the socket &
* its receipt at the client
*
* - player buffering: the player may buffer sound data (mplayer, for instance,
* buffers half a second's worth of audio before beginning playback by
* default)
*
* - render time: the client presumably wishes the current frame to appear
* on-screen at the moment the current sound information is ending
*
* Throughout, let \e t be "song time" be measured on the server, and T(t) be
* sound information for song time \e t. Let FPS be the frames-per-second at
* which the client would like to render.
*
* Then, at an interval of 1/FPS seconds, the server needs to write
*
\verbatim
T(t - {buffer time} + {render time} + {one way latency})
\endverbatim
*
* to the client socket. If we denote that time offset (i.e. the render time +
* one-way latency minus the buffer time) by tau, then the server should wait
* max(0, -tau) ms to write the first frame.
*
* A few examples will illustrate.
*
* \subsection vis_out_protocol_timing_eg_1 Example 1
*
* Let the client render time be 4ms and round-trip network latency be
* 6ms. Assume no player buffering. In order to render a frame corresponding to
* song time \e t, the client would need, at time \e t - 4 ms, sound information
* corresponding to time \e t, or T(t). The server would need to \e send that
* information at time \e t - 7ms (half of one round-trip plus render time).
*
* In other words, on the server side at song time \e t, we would need to write
* T(t + 7ms) to the client socket. If the server writes T(t+7ms) immediately,
* the client will receive it at \e t + 4ms, take 4ms to render the next frame,
* and so at \e t + 7ms hence, finish rendering T(t+7).
*
* \subsection vis_out_protocol_timing_eg_2 Example 2
*
* Imagine we are running the same client on a laptop, connected to an MPD
* server over the internet, and using mplayer as the player. This gives 500ms
* of buffer time. Let us assume the same 4ms render time, but now a 20ms
* round-trip time.
*
* In order to render a frame corresponding to song time \e t, the client would
* need, at time \e t - 4ms, T(t). This would need to be sent from the server at
* time \e t - 14ms. We now need to incorporate the client-side buffering,
* however. Song time \e t will be actually played on the client at \e t + 500ms
* on the server.
*
* In other words, on the server side at song time \e t, we would need to write
* T(t-486ms) to the client socket.
*
* Since the sound won't start on the client for 0.5 sec, it would make no sense
* to begin writing sound information for 486ms. Let t(0) be the moment the
* client connects and the player begins buffering. If, at t(0) + 486ms, the
* server writes T(t(0)), the client will receive it at t(0) + 496ms & complete
* rendering it at t(0) + 500ms, which is when the client-side player will
* begin playing song time t(0).
*
* \section vis_out_protocol_proto The Protocol
*
* \subsection vis_out_protocol_proto_design Design
*
* The author is unaware of any existing network protocols in this area, so he
* designed his own after reviewing the Shoutcast & Ultravox
* protocols. Experience with the TLS & 802.11 protocols also informed this
* design.
*
* Design goals include:
*
* - client convenience
* - this in particular drove the choice to stream updates; everything
* needed to simply push the data out is knowable at handshake time,
* so why force the client to send a request?
* - efficiency on the wire
* - binary format
* - streaming preferred over request/response
* - future extensibility
* - protocol versioning built-in from the start
* - parsing convenience
* - streaming messages come with a few "magic bytes" at the start
* to assist clients in "locking on" to the stream & recovering from
* corrupted data, client-side disruptions & so forth
* - all messages conform to the "type-length-value" (TLV) format
* beloved of parser writers
*
* Responses to the intial
* <a href="https://github.com/MusicPlayerDaemon/MPD/pull/1449">RFC</a> also
* informed the protocol's first implementation: I've stripped out all but the
* essentials in pursuit of a minimally effective protocol that is still
* extensible should it prove useful
*
*
* \subsection vis_out_protocol_proto_overview Overview
*
* The protocol is a combination of request/response as well as streaming. After
* an initial handshake (client goes first) the server will begin streaming
* messages to the client; i.e. at the interval the client specified during the
* initial handshake the server will send FRAME messages containing sound
* information useful for visualizers. The client need not request these
* messages or does the client need to acknowledge them in any way.
*
* Schematically, a conversation looks like this:
*
\verbatim
Client Server
desired protocol version
tau (buffer offset)
frame rate --------- CLIHLO --------->
...
<-------- SRVHLO --------- offered protocol version
<-------- FRAME --------- samples, spectrum
| 1/fps sec
<-------- FRAME --------- samples, spectrum
...
(forever)
\endverbatim
*
* There is no formal "close" or "teardown" message; each side simply detects
* when the other has gone away & treats that as the end of the conversation.
*
*
* \subsection vis_out_protocol_proto_msgs Messages
*
* All messages:
*
* - integers use network byte order (i.e. big endian)
* - use TLV format (streaming messages prepend magic bytes)
*
\verbatim
+---------+-----------------------+-----------------+-----------------------+--------+
|(prefix) | TYPE (16-bit unsigned)| LENGTH | PAYLOAD | CHECK |
| | class | message type | 16-bits unsigned| LENGTH bytes | 1 byte |
|---------|-------+---------------|-----------------|-----------------------+--------+
|63ac84003| 4 bits| 12 bits | (max len 65535) | format is msg-specfic | 00 |
+---------+-----------------------+-----------------+-----------------------+--------+
\endverbatim
*
* Notes:
*
* - the prefix is only prepended to FRAME messages to enable clients to "lock
* on" to a pre-existing stream of data; 0x63ac4003 were the first four bytes
* I pulled from \c /dev/urandom on my dev workstation on Monday, September 04.
*
* - the message type is comprised of two values packed into a u16_t:
*
* - class: (type & 0xf000) >> 12:
* - 00: handshake
* - 01: streaming (FRAME, e.g.)
*
* - message type: (type & 0ffff) see below for values
*
* - the "length" field is the length of the \e payload \e only (i.e. \e not the
* length of the entire message)
*
* - the "check" byte is intended as a sanity test & shall always be zero
* Although, what would the client do if the check failed? There's no
* provision in this protocol to re-request the frame. Discard it, I suppose.
*
* The following subsections define the PAYLOAD portion of the above messages.
*
* \subsubsection vis_out_protocol_proto_clihlo CLIHLO
*
* No prefix. The class is 0x0 (handshake) & the message type is 0x000.
*
* Payload:
*
\verbatim
+---------------+---------------+---------------+---------------+
| major version | minor version | requested FPS | requested TAU |
| ------------- | ------------- |-------------- |---------------+
| uint8_t | uint8_t | uint16_t | int16_t |
+---------------+---------------+---------------+---------------+
\endverbatim
*
* Payload size: 6 octets
*
* \subsubsection vis_out_protocol_proto_srvhlo SRVHLO
*
* No prefix. The class is 0x0 (handshake) & the message type is 0x001.
*
* Payload:
*
\verbatim
+---------------+---------------+
| major version | minor version |
| ------------- | ------------- |
| uint8_t | uint8_t |
+---------------+---------------+
\endverbatim
*
* \subsubsection vis_out_protocol_proto_frame FRAME
*
* Prefix. The class is 0x1 (streaming) & the message type is 0x000.
*
* Below, \c float denotes a floating-point value, expressed in IEEE 754
* single-precision format, in big-endian byte order. \c complex denotes a pair
* of floating-point values (the real & imaginary components of a complex
* number, in that order) in the same format.
*
* Payload:
*
\code
+----------+----------+-------------+-----------+----------+---------+---------+----------+------------+---------------+-----------------+
| num_samp | num_chan | sample_rate | waveforms | num_freq | freq_lo | freq_hi | freq_off | coeffs | power_spectra | bass/mids/trebs |
| -------- | -------- | ----------- | --------- | -------- | ------- | ------- | -------- | ---------- | ------------- | --------------- |
| uint16_t | uint8_t | uint16_t | see below | uint16_t | float | float | uint16_t | see below | see below | see below |
+----------+----------+-------------+-----------+----------+---------+---------+----------+------------+---------------+-----------------+
waveforms:
+----------------------+----------------------+-----+---------------------------------+
| waveform for chan. 0 | waveform for chan. 1 | ... | waveform for chan. num_chan - 1 |
| -------------------- | -------------------- | ... | ------------------------------- |
| float | ... | float | float | ... | float | ... | float | ... | float |
| -------------------- | -------------------- | ... | ------------------------------- |
| (num_samp floats) | (num_samp floats) | ... | (num_samp floats) |
+----------------------+----------------------+-----+---------------------------------+
total: num_samp * num_chan * 4 octets
coeffs:
+--------------------------+--------------------------+-----+-------------------------------------+
| freq. domain for chan. 0 | freq. domain for chan 1. | ... | freq. domain for chan. num_chan - 1 |
| ------------------------ + -------------------------+---- + ----------------------------------- |
| complex | ... | complex | complex | ... | complex | ... | complex | complex | ... | complex |
| ------------------------ +--------------------------+-----+-------------------------------------|
| num_freq complex | num_freq complex | ... | num_freq complex |
+--------------------------+--------------------------+-----+-------------------------------------+
total: num_chan * num_freq * 8 octets
power spectra:
+-----------------------------+-----+---------------------------------------+
| power spectrum for chan. 0 | ... | power spectrum for chan. num_chan - 1 |
| --------------------------- +-----+ ------------------------------------- |
| float | float | ... | float | ... | float | float | ... | float |
| --------------------------- + --- + ------------------------------------- |
| num_freq floats | ... | num_freq floats |
+-----------------------------+-----+---------------------------------------+
total: num_chan * num_freq * 4 octets
bass/mids/trebs
+-----------------------------+-----+----------------------------------------+
| bass/mids/trebs for chan. 0 | ... | bass/mids/trebs for chan. num_chan - 1 |
| --------------------------- +-----+ -------------------------------------- |
| float | float | float | ... | float | float | float |
+-----------------------------+-----+----------------------------------------+
total: num_chan * 12 octets
payload size: 17 + num_samp * num_chan * 4 + num_chan * num_freq * 8 + num_chan * num_freq * 4 + num_chan * 12
= 17 + 4 * num_chan * (num_samp + 3 * num_freq + 3)
\endcode
*
* - \c num_samp: the number of audio samples used in this analysis: this is set
* in plugin confiugration and in practice needn't be particularly large (512
* is the default setting). This determines the number of values in
* \c waveforms, and in part the number of values in \c frequencies and
* \c power_spectra (see below)
*
* - \c num_chan: the number of audio channels used in this analysis: this is
* determined by the audio stream being played at any given time, but 2
* (i.e. stereo) is typical
*
* - \c sample_rate: the number of samples per second at which this audio stream
* is encoded (44100 is typical)
*
* - \c waveforms: the PCM data on which this analysis was based; there will be
* \c num_chan sets of num_samp floats (one for each channel, arranged one
* after the other; i.e. not interleaved)
*
* - \c num_freq: the number of frequency values returned for each waveform in
* this frame; this is a function the sample rate, the number of audio
* samples, and the frequency cutoffs with which the plugin was configured (on
* which more below)
*
* - \c freq_lo, \c freq_hi: the frequency range returned; this is set in plugin
* configuration. The range of human perception is roughly 200Hz to 20,000Hz,
* but in practice musical sound data contains little information above 10-12K
* Hz, so a typical setting for this range is 200Hz and 10000Hz.
*
* - \c freq_off: the index corresponding to \c freq_lo; this can be used by the
* caller to map a Fourier coefficient to a frequency (see \c coeffs, below)
*
* - \c coeffs: the Fourier coefficients for each waveform, expressed as complex
* numbers; the i-th value in this range is the \c freq_off + \c i -th Fourier
* coefficient, corresponding to a frequency of
*
\code
(freq_off + i) * samp_rate
--------------------------- Hz
num_samp
\endcode
*
* The reason for this convention is that the plugin will _only_ return the
* Fourier coefficients within the ranage defined by \c freq_lo & \c freq_hi.
*
* Note that Discrete Fourier Transforms of real-valued series (such as our PCM
* waveform) display the Hermitian property:
*
\code
*
C(i) = C(n-i)
\endcode
*
* where '*' denotes complex conjugation. Many libraries take advantage of this
* to save space by only returning the first n/2 + 1 Fourier coefficients (since
* the remaining coefficients can be readily computed from those). The
* application of a frequency window spoils this nice symmetry.
*
* - \c power_spectra: the power spectrum for each channel; this is merely the
* magnitude of the Fourier coefficent at each frequency. Strictly speaking
* the client could compute this for themselves, but this is such a frequently
* used value the plugin computes & transmits it as a convenience to the
* caller, There are again \c num_freq values.
*
* - bass/mids/trebs: once the frequency domain is truncated to the given
* bounds, the number of octaves therein is divided into three equal
* bands and the power in each band is summed (this is done separately
* for each channel)
*
* A number of these quantities won't change; they're defined in plugin
* configuration; \c num_samp, \c freq_lo & \c freq_hi could, in principle, be
* moved to the SRVHLO message.
*
* Furthermore, \c num_chan, \c sample_rate and hence \c num_freq are set at the
* start of each new audio stream, and so could be communicated once at that
* point & omitted from subsequent frames.
*
* That said, this would complicate client implementations for the sake of
* saving a few bytes on the wire; I've chosen to simply communicate this
* information in each frame.
*
*
*/
/**
* \page vis_out_arch Layout of the Visualization Output Plugin
*
* \section vis_out_arch_intro Introduction
*
* There are, at the time of this writing, two other output plugins that provide
* socket servers: HttpdOutput & SnapcastOutput. They both follow a similar
* pattern in which the plugin subclasses both AudioOutput \e and
* ServerSocket. Since I have chosen a different approach, I should both
* describe the layout of VisualizationOutput and explain my choice.
*
* \section vis_out_arch_cyclic Cyclic Dependencies
*
* While they subclass privately (implying an "implemented-in-terms-of" rather
* than "is-a" relationship with their superclasses), HttpdOutput &
* SnapcastOutput in practice handle the duties of being both an AudioOutput and
* a ServerSocket. This introduces not one but two cyclic dependencies in their
* implementations:
*
* 1. the ServerSocket half of them is responsible for creating new clients, but
* the clients are the ones who detect that their socket has been closed; they
* then need a back-reference to signal their parent that they should be
* destroyed (by calling RemoveClient() through their back-reference).
*
* 2. the AudioOutput half of them is responsible for pushing new data derived
* from PCM data out to all their clients, while their clients request
* information & service from their parent, again requiring a back reference
* (GetCodecName() on the Snapcast client, e.g.)
*
* Cyclic dependencies carry with them drawbacks:
*
* - they increase compilation times because when one file in the cycle is
* changed, all the other translation units need to be recompiled
*
* - they increase coupling, increasing the chances that a change in
* one place will break others
*
* - code reuse becomes more difficult-- trying to hoist one file out involves
* bringing all the other files in the cycle along with it
*
* - unit testing becomes harder-- the smallest unit of testable
* funcationality becomes the union all the the translation units in the
* cycle
*
* \section vis_out_arch_threads Too Many Threads!
*
* This arrangement entails another problem: HttpdOutput & SnapcastOutput
* instances have their methods invoked on two threads; the main I/O thread as
* well as the player control thread. This means that access to some state needs
* to be guarded by a mutex (in the case of HttpdOutput, the client list & the
* pages), but \e not others (again in the case of HttpdOutput, content or
* genre).
*
* \section vis_out_arch_demotion Breaking Dependency Cyles Through Demotion
*
* I instead chose to have VisualizationOutput \e be an AudioOutput, and \e own
* a ServerSocket. The state & behavior required by both is pushed down into
* class SoundInfoCache on which both depend. This arrangement breaks things up
* in a few ways.
*
* Cycle 1 is broken up by having a one-way relationship only between the socket
* server & clients. When a client detects that its socket has been closed, it
* marks itself "dead" and will eventually be reaped by the server.
*
* Cycle 2 is broken by Lakos' method of "demotion": the functionality required
* by both the output plugin & the various clients is pushed down into a
* separate class SoundInfoCache. It is owned by the plugin, and referenced by
* clients. When the plugin is disabled, the plugin is responsible for
* cleaning-up the server, which will in turn clean-up all the clients, and only
* then destroying the SoundInfoCache instance.
*
* In ASCII art:
*
\verbatim
sound +---------------------+ +---------------------+
-- data ----> | VisualizationOutput | --- owns ---> | VisualizationServer |
+---------------------+ +---------------------+
| Play() | | OnAccept() |
+---------------------+ +---------------------+
1 | | 1
| +---owns----+
| |
| v *
| +---------------------+
owns | VisualizationClient |
| +---------------------+
| | *
| +----references------+
| |
1 v v 1
+----------------+
| SoundInfoCache |
+----------------+
\endverbatim
*
* This arrangement also addresses the threading issue: other than creation &
* destruction, the socket server has all of its methods invoked on the I/O
* thread, and those of the plugin on the player control thread. The state that
* needs to be guarded against access from multiple threads is localized in
* SoundInfoCache.
*
*
* \section vis_out_arch_promotion A Discarded Approach
*
* The \ref vis_out_back "idea" of having sound analysis accessible through the
* MPD client
* <a href="https://mpd.readthedocs.io/en/latest/protocol.html">protocol</a>
* to me begged the question: why not have SoundInfoCache be owned directly by
* MultipleOutputs? MPD clients could make requests directly via
*
\code
partition.outputs.sound_info_cache.analyze(...);
\endcode
*
* We could hand a reference to it to the visualization output plugin, and have
* the plugin be solely responsible for serving the network protocol.
*
* I saw a few advantages to this:
*
* 1. Convenient access for the implementations of MPD client protocol commands
*
* 2. Users could get sound analysis via the MPD client protocol without having
* to configure & enable an output plugin
*
* 3. General simplification-- the output plugin would only be responsible
* for serving the network protocol
*
* All that said, I discarded this approach. If I wanted the sound analysis to
* receive sound data post-cross-fade, post-replay gain and after any other
* filtering, it was going to need to own an AudioOutputSource instance. Thing
* is, when I open an AudioOutputSource I need:
*
* - the AudioFormat
* - a reference to the MusicPipe
* - the ReplayGain filter(s)
* - any other filters
*
* MultipleOutputs doesn't know these; it's just got a bunch of
* configuration. The configuration gets turned into these objects in
* FilteredAudioOutput::Setup() and it's non-trivial to do so. The plumbing is
* complex enough that I'm inclined to leave it where it is. So now we're at a
* point where SoundInfoCache would need to own both an AudioOutputSource \e and
* a FilteredAudioOutput... at which point it starts to look very much like an
* AudioOutputControl (in other words, just another audio output under
* MultipleOutputs).
*
*
*/
/**
* \class VisualizationOutput
*
* \brief An output plugin that serves data useful for music visualizers
*
* \sa \ref vis_out_plugin_arch "Architecture"
*
*
* Both the fifo & pipe output plugins can be used to directly access the PCM
* audio data, and so can (and have been) used to implement music visualizers
* for MPD. They are, however, limited to clients running on the same host as
* MPD. This output plugin will stream PCM samples along with derived
* information useful for visualizers (the Fourier transform, bass/mids/trebs,
* and so forth) over one or more network connections, to allow true MPD client
* visualizers.
*
*
*/
class VisualizationOutput: public AudioOutput {
/* When the plugin is enabled, we actually "open" the server (which is
* to say, bind the socket & begin accepting incoming connections) */
VisualizationServer server;
/* This will be null unless the plugin is open; it's a `shared_ptr`
* because we share references with the socket servers and the
* `VisualizationClient` instances representing active connections */
std::shared_ptr<SoundInfoCache> pcache;
/// The number of seconds' worth of audio data to be cached
std::chrono::seconds cache_duration;
public:
static AudioOutput* Create(EventLoop &event_loop,
const ConfigBlock &cfg_block) {
return new VisualizationOutput(event_loop, cfg_block);
}
VisualizationOutput(EventLoop &event_loop,
const ConfigBlock &cfg_block);
virtual ~VisualizationOutput() override; // We have virtuals, so...
public:
////////////////////////////////////////////////////////////////////////
// AudioOutput Interface //
////////////////////////////////////////////////////////////////////////
/**
* Enable the device. This may allocate resources, preparing
* for the device to be opened.
*
* Throws on error.
*/
virtual void Enable() override;
/**
* Disables the device. It is closed before this method is called.
*/
virtual void Disable() noexcept override;
/**
* Really open the device-- mandatory.
*
* Throws on error.
*
* @param audio_format the audio format in which data is going
* to be delivered; may be modified by the plugin
*/
virtual void Open(AudioFormat &audio_format) override;
/**
* Close the device-- mandatory.
*/
virtual void Close() noexcept override;
/**
* Play a chunk of audio data-- mandatory. The method blocks until at
* least one audio frame is consumed.
*
* Throws on error.
*
* May throw #AudioOutputInterrupted after Interrupt() has
* been called.
*
* @return the number of bytes played (must be a multiple of
* the frame size)
*/
virtual size_t Play(std::span<const std::byte> src) override;
};
} // namespace Visualization
using std::make_unique;
const Domain vis_output_domain("vis_output");
Visualization::VisualizationOutput::VisualizationOutput(
EventLoop &event_loop,
const ConfigBlock &config_block):
AudioOutput(FLAG_ENABLE_DISABLE | FLAG_PAUSE),
server(event_loop,
config_block.GetBlockValue("bind_to_address"),
config_block.GetBlockValue("port", 8001U),
config_block.GetPositiveValue("max_clients", 0),
Visualization::SoundAnalysisParameters(config_block)),
cache_duration(config_block.GetPositiveValue("cache_duration", 1))
{ }
Visualization::VisualizationOutput::~VisualizationOutput()
{ }
void
Visualization::VisualizationOutput::Enable() {
FmtInfo(vis_output_domain, "VisualizationOutput::Enable({})", std::this_thread::get_id());
BlockingCall(server.GetEventLoop(), [this](){
server.Open();
});
}
void
Visualization::VisualizationOutput::Disable() noexcept {
FmtInfo(vis_output_domain, "VisualizationOutput::Disable({})", std::this_thread::get_id());
BlockingCall(server.GetEventLoop(), [this](){
server.Close();
});
}
void
Visualization::VisualizationOutput::Open(AudioFormat &audio_format)
{
FmtInfo(vis_output_domain, "VisualizationOutput::Open({})", std::this_thread::get_id());
/* At this point, we know the audio format, so we can at this point
* instantiate the PCM data cache. */
pcache = make_shared<Visualization::SoundInfoCache>(audio_format,
cache_duration);
BlockingCall(server.GetEventLoop(), [this]() {
server.OnPluginOpened(pcache);
});
}
void
Visualization::VisualizationOutput::Close() noexcept
{
FmtInfo(vis_output_domain, "VisualizationOutput::Close({})", std::this_thread::get_id());
BlockingCall(server.GetEventLoop(), [this]() {
server.OnPluginClosed();
});
pcache = nullptr;
}
size_t
Visualization::VisualizationOutput::Play(const std::span<const std::byte> src)
{
pcache->Add(src.data(), src.size());
return src.size();
}
const struct AudioOutputPlugin visualization_output_plugin = {
"visualization",
nullptr, // cannot serve as the default output
&Visualization::VisualizationOutput::Create,
nullptr, // no particular mixer
};