-
Notifications
You must be signed in to change notification settings - Fork 0
/
tips.html
1153 lines (861 loc) · 28.3 KB
/
tips.html
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
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<!--
/*
* Copyright (c) 2008-2011 Alexandre Ratchov <[email protected]>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
-->
<html>
<head>
<title>sndio - hints on writing & porting audio code</title>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
<meta name="language" content="en">
<style type="text/css">
@media screen, print {
h1 {
text-align: center
}
table {
border-style: solid;
border-width: thin;
border-collapse: collapse;
margin-left: auto;
margin-right: auto;
}
th {
border-style: solid;
border-width: thin;
padding: 0.5em;
}
td {
border-style: solid;
border-width: thin;
padding: 0.5em;
}
pre {
padding-top: 1em;
padding-bottom: 1em;
padding-left: 4em;
padding-right: 4em;
}
dt {
font-weight: bold;
}
dd {
padding: 0.5em;
}
}
@media screen {
body {
margin-top: 1em;
margin-bottom: 1em;
margin-left: auto;
margin-right: auto;
padding-top: 1em;
padding-bottom: 0em;
padding-left: 1em;
padding-right: 1em;
border-style: dashed;
border-top-width: thin;
border-bottom-width: thin;
border-left-width: thin;
border-right-width: thin;
position: relative;
max-width: 44em;
border-color: #ccc;
background-color: #fff;
}
th {
background-color: #eee
}
pre {
background-color: #eee
}
}
@media print {
pre {
border-style: dotted;
border-width: thin;
}
}
</style>
</head>
<body>
<h1>sndio<br>hints on writing & porting audio code</h1>
<!-- toc_begin - automatically generated, don't modify -->
<h2>Table of contents</h2>
<ul>
<li><a href="#section_1">1 Introduction</a>
<ul>
<li><a href="#section_1_1">1.1 Aim of this document</a>
<li><a href="#section_1_2">1.2 Device model overview</a>
</ul>
<li><a href="#section_2">2 Parameter negotiation</a>
<ul>
<li><a href="#section_2_1">2.1 Selecting formats and encodings</a>
</ul>
<li><a href="#section_3">3 Choosing the buffer size</a>
<li><a href="#section_4">4 Synchronizing stuff on audio playback</a>
<ul>
<li><a href="#section_4_1">4.1 Absolute play and record positions</a>
<li><a href="#section_4_2">4.2 Playback latency à la GET_ODELAY</a>
<li><a href="#section_4_3">4.3 Playback buffer usage à la GET_OSPACE</a>
<li><a href="#section_4_4">4.4 Record buffer usage à la GET_ISPACE</a>
<li><a href="#section_4_5">4.5 Sleeping until there's space for one block in the play buffer</a>
</ul>
<li><a href="#section_5">5 Choosing and using the block size</a>
<ul>
<li><a href="#section_5_1">5.1 Getting the block size to optimize I/O</a>
<li><a href="#section_5_2">5.2 Using a small block size for low latency</a>
<li><a href="#section_5_3">5.3 Getting higher clock resolution for synchronization</a>
</ul>
<li><a href="#section_6">6 Adjusting the volume, mute knob</a>
<ul>
<li><a href="#section_6_1">6.1 Setting the volume</a>
<li><a href="#section_6_2">6.2 Volume feedback (reading the current volume 1)</a>
<li><a href="#section_6_3">6.3 Volume getter (reading the current volume 2)</a>
</ul>
<li><a href="#section_7">7 Pausing and resuming</a>
<li><a href="#section_8">8 Using sndio in multi-threaded programs</a>
<li><a href="#section_9">9 Windows-style callbacks</a>
<li><a href="#section_10">10 Pitfalls</a>
<ul>
<li><a href="#section_10_1">10.1 Playback and record aren't independent</a>
<li><a href="#section_10_2">10.2 Using appbufsz instead of bufsz</a>
<li><a href="#section_10_3">10.3 How many bytes to store a 24-bit sample</a>
<li><a href="#section_10_4">10.4 poll(2) not called fast enough</a>
</ul>
<li><a href="#section_11">11 Glossary</a>
</ul>
<!-- toc_end -->
<h2><a name="section_1">1 Introduction</a></h2>
<h3><a name="section_1_1">1.1 Aim of this document</a></h3>
<p>
This document contains simple tips on how to write new code for the
sndio API as well as how to port existing code to it.
This document doesn't explain how to invoke sndio functions, which are
already described in the sio_open(3) manual page.
<p>
Remember to keep things as simple as possible; the sndio API is
designed to make this possible.
If something looks complicated, the approach may be wrong.
In some cases, it may be better to drop some complicated feature rather
than adding hackish code that may hurt the overall correctness and
robustness of the application.
<h3><a name="section_1_2">1.2 Device model overview</a></h3>
<p>
The sndio device model is as follows:
<ul>
<li>
A bidirectional data stream exists between the program and the
sound-card, for the <i>play</i> and <i>record</i> directions,
respectively.
<li>
Data is a sequence of frames, where each frame corresponds to a
sample for all channels of the stream.
It is submitted and retrieved using functions similar to the read(2)
and write(2) syscalls.
<li>
A wall clock ticks when samples are processed by the hardware; i.e.
the <i>n</i>-th frame of the stream corresponds to the <i>n</i>-th
clock tick.
The clock is exposed through a callback mechanism: a function
registered by the program is called periodically, which takes as argument
the number of clock ticks elapsed since the last call.
</ul>
In other words, the <i>n</i>-th sample read is recorded exactly when
the <i>n</i>-th written sample is played.
This means that samples to play must be written before
recorded samples can be read, otherwise a deadlock will occur.
<h2><a name="section_2">2 Parameter negotiation</a></h2>
To minimize mistakes, the following approach can
be used:
<ol>
<li>
call sio_setpar(3) using the application's native parameters.
<li>
call sio_getpar(3) and verify whether returned parameters
are usable.
</ol>
<p>
Certain applications support multiple parameters sets, so if the above
steps failed, you may want to retry with another set. However, that's
unlikely to work in real life for two reasons:
<ul>
<li>
apps often support "common" formats that "common" hardware
supports, so if it didn't work, it's probably because the
hardware is not so "common". Therefore, trying another "common"
format the application supports has little chance of working
<li>
that's more code, so greater chance of introducing a bug. Why? To allow
the app to emulate the format one particular piece of
hardware supports. Is it worth the effort given that sndiod(8)
already does emulation and supports <b>any</b> hardware
and is always enabled by default?
</ul>
<h3><a name="section_2_1">2.1 Selecting formats and encodings</a></h3>
A typical example is a simple player that tries to play 2-channel, s16
at 44.1kHz. If the audio subsystem doesn't support this format it should
just fail, so the following is OK:
<pre>
...
par.pchan = 2;
par.sig = 1;
par.bits = 16;
par.le = SIO_LE_NATIVE;
par.rate = 44100;
if (!sio_setpar(hdl, &par))
errx(1, "internal error, sio_setpar() failed");
if (!sio_getpar(hdl, &par))
errx(1, "internal error, sio_getpar() failed");
if (par.pchan != 2)
errx(1, "couldn't set number of channels");
if (!par.sig || par.bits != 16 || par.le != SIO_LE_NATIVE)
errx(1, "couldn't set format");
if (par.bits != 16 || par.bps != 2)
errx(1, "couldn't set precision");
if (par.rate < 44100 * 995 / 1000 ||
par.rate > 44100 * 1005 / 1000)
errx(1, "couldn't set rate");
...
</pre>
As sndiod(8) is used by default, sio_setpar(3) will always use the requested
parameters.
If the user has requested direct access to the hardware, then
sio_setpar(3) may configure the device to other parameters, so the new ones
must be checked with sio_getpar(3).
<h2><a name="section_3">3 Choosing the buffer size</a></h2>
The buffer size represents the amount of time given to the
application to produce data to play (or to consume recorded data).
If the application doesn't respect this constraint, xruns will occur.
<p>
Therefore, we must estimate the maximum time it will take to
prepare the data and to fill the buffer, and then choose a slightly larger
buffer size by setting the appbufsz parameter in the sio_par
structure.
<p>
On a multitasking system, the delay estimate must take into account
the other processes hogging the system.
On a typical Unix-like system, a margin of around
~5-10ms seems OK. If the buffer size is not set, the audio subsystem
will choose a reasonable value, something around 50ms.
<p>
For example, consider a file player. It's organized as follows:
<pre>
for (;;) {
read_file_to_fifo();
play_from_fifo();
}
</pre>
The maximum time it takes for the application to call play_from_fifo()
is roughly equal to the maximum time read_file_to_fifo() takes to
complete. Reading from a file may block for around 50ms, so around
100ms of buffer is mostly OK. If the file uses a 44.1kHz sampling
rate, then the buffer size is:
<pre>
0.1s * 44100Hz = 4410 frames
</pre>
The orders of magnitudes of the maximum delay for different operations,
measured on a slow i386 system with ~2 users doing simple stuff
(editors, basic X11, compilations), can be seen below:
<p align="center">
<table>
<tr>
<th>operation
<th>max delay
<tr>
<td>extract a block from a CD
<td>300ms
<tr>
<td>read less than 64kB from hard disk
<td>50ms
<tr>
<td>read from a pipe + pair of context switches
<td>10ms
</table>
<p>
<b>Note</b>: the device may choose a different buffer size that the one the
application requested.
In any case, the application must use sio_getpar(3) and take into account the
actual buffer size.
<h2><a name="section_4">4 Synchronizing stuff on audio playback</a></h2>
<p>
Timing information is available by setting up a callback
with the sio_onmove(3) function:
<pre>
struct sio_par par;
long long writecnt; /* frames written (in bytes) */
long long readcnt; /* frames read (in bytes) */
long long realpos; /* frame number Joe is hearing */
void
cb(void *addr, int delta)
{
realpos += delta;
}
int
main(void)
{
sio_hdl *hdl;
sio_par par;
...
writecnt = readcnt = realpos = 0;
sio_onmove(hdl, cb, NULL);
...
for (;;) {
...
writepos += sio_write(hdl, buf, count);
...
readpos += sio_read(hdl, buf, count);
...
}
...
}
</pre>
The callback is invoked every time a block is processed by the hardware.
It's called from one of the following functions:
<ul>
<li>sio_revents(3) after poll(2)
<li>blocking sio_write(3) and sio_read(3)
</ul>
<h3><a name="section_4_1">4.1 Absolute play and record positions</a></h3>
<p>
The absolute play position is given by realpos, from the above example.
If the application needs this expressed in seconds:
<pre>
realpos_sec = realpos / par.rate;
</pre>
Note that in earlier versions of sndio, ``realpos'' could be
negative, but that feature was removed.
<h3><a name="section_4_2">4.2 Playback latency à la GET_ODELAY</a></h3>
The playback latency is the delay (expressed in number of frames) that
it will take until the last frame that was written becomes audible.
This is exactly the buffer usage:
<pre>
writepos = writecnt / (par.pchan * par.bps); /* convert to frames */
bufused = writepos - realpos;
</pre>
The recording latency is generally zero, since the application is
waiting and consuming the data immediately.
<h3><a name="section_4_3">4.3 Playback buffer usage à la GET_OSPACE</a></h3>
<p>
Certain applications ask for the number of bytes left in the playback
buffer, assuming that sio_write(3) will not block if the program writes
less than the space available in the buffer.
<b>This is wrong</b>, but sometimes it's not desirable to change the
application, so the available buffer space could be calculated as follows:
<pre>
space_avail = par.bufsz - bufused;
</pre>
<h3><a name="section_4_4">4.4 Record buffer usage à la GET_ISPACE</a></h3>
Using this for non-blocking I/O is wrong too,
nevertheless the buffer usage is:
<pre>
readpos = readcnt / (par.rchan * par.bps);
bufused = realpos - readpos;
</pre>
<h3><a name="section_4_5">4.5 Sleeping until there's space for one block in the play buffer</a></h3>
<p>
Certain applications want to sleep until there's space
for at least one block in the play buffer.
There's no way to wait for such an event, and that's not compatible
with Unix file semantics.
<p>
The best approach is to change the application to use poll(2).
If that's not possible, wait until the stream is writable as follows:
<pre>
void
wait_space_avail(void)
{
int nfds, revents;
struct pollfd *pfds = malloc(sio_nfds(hdl) * sizeof(*pfds));
do {
nfds = sio_pollfd(hdl, pfds, POLLOUT);
if (nfds > 0) {
if (poll(pfds, nfds, -1) < 0)
err(1, "poll failed");
}
revents = sio_revents(hdl, pfds);
if (revents & POLLHUP)
errx(1, "device disappeared");
} while (!(revents & POLLOUT));
}
</pre>
Other approaches would probably lead to stuttering or to a busy loop,
which, in turn, may lead to stuttering.
<p>
Note, however, that if poll(2) is called with no file descriptors
and non-zero timeout, it will hang, and if timeout is negative, it
will hang forever. That means we need to check if nfds is positive.
<h2><a name="section_5">5 Choosing and using the block size</a></h2>
<h3><a name="section_5_1">5.1 Getting the block size to optimize I/O</a></h3>
<p>
Audio is a continuous stream of frames, but the hardware processes
them in blocks. A typical player will have an internal ring that will
be filled by the player and consumed using sio_write(3). If the ring
size is a multiple of the hardware block size, then calls to
sio_write(3) will be optimal.
<p>
The block size is stored in the ``round'' field of the sio_par
structure, and is negotiated using sio_setpar(3) and sio_getpar(3).
Application should round their internal buffer sizes as follows:
<pre>
buf_size = desired_buf_size + par.round - 1;
buf_size -= buf_size % par.round;
</pre>
The ``round'' parameter is very constrained by the hardware, so
sio_setpar(3) only uses it as a hint.
<h3><a name="section_5_2">5.2 Using a small block size for low latency</a></h3>
The minimum latency a program can get is related to the minimum buffer
size, which is often one or two blocks. So if an application needs very
low latency, it must use a small block size too, but there's no need to
change it explicitly.
<p>
When changing the ``appbufsz'' parameter, an optimal block size is
calculated by the sio_setpar(3) function. The sio_setpar(3) function
will evolve to cope with future hardware and software constraints, so
it's expected to always do the right thing, on any hardware. Therefore,
in order to get the maximum robustness, don't change the block size.
<h3><a name="section_5_3">5.3 Getting higher clock resolution for synchronization</a></h3>
<p>
Synchronization is based on the callback set with the sio_onmove(3)
function. It's called periodically, every time a block is
processed. Basically, this provides clock ticks to the program,
which correspond to the sound card's clock.
<p>
If the block size is large, the tick rate is low, and time increases in
big steps, which may not be desirable for applications requiring higher
clock resolution.
The easiest solution is to use a smaller block size to get a higher
tick rate. This approach has the advantage of being very accurate,
but it's CPU intensive. It's also not always possible to choose the
block size (e.g. because of hardware constraints).
<p>
Example: a video player plays 25 images per second. To get a smooth
video, images must be displayed at regular time intervals. Thus, the
clock resolution must be at least twice the image rate, i.e. 50 ticks
per second. If the audio rate is 44.1kHz, the maximum block size to get
smooth video is:
<pre>
44100Hz / 50Hz = 882 frames per block
</pre>
Another solution is to use a large block size, and extrapolate the
time between clock ticks using gettimeofday(2). This is more
complicated to get right, but works in all situations, is less CPU
intensive and works even if very high clock resolution is needed.
<h2><a name="section_6">6 Adjusting the volume, mute knob</a></h2>
<h3><a name="section_6_1">6.1 Setting the volume</a></h3>
<p>
It's as simple as calling sio_setvol(3) with a value in the 0..127
range, where 0 means ``mute the stream'' and 127 is the maximum volume
(the default).
Certain apps use percents in the 0..100 range, in that case a conversion
must be performed as follows:
<pre>
#define PCT_TO_SIO(pct) ((127 * (pct) + 50) / 100)
#define SIO_TO_PCT(vol) ((100 * (vol) + 64) / 127)
void setvol(int p)
{
...
sio_setvol(hdl, PCT_TO_SIO(p));
}
</pre>
<h3><a name="section_6_2">6.2 Volume feedback (reading the current volume 1)</a></h3>
<p>
There's no getter for the current volume; instead the program
can install a callback to be notified about volume changes:
<pre>
void
cb(void *addr, unsigned vol)
{
redraw_volume_slider(SIO_TO_PCT(vol));
}
int
main(void)
{
...
sio_onvol(hdl, cb, NULL);
...
for (;;) {
p = mouse_event_to_pct();
setvol(p);
}
}
</pre>
<h3><a name="section_6_3">6.3 Volume getter (reading the current volume 2)</a></h3>
<p>
Certain applications require a ``get volume'' function and
work as follows:
<pre>
for (;;) {
p = volume_slider_to_pct();
setvol(p);
p = getvol();
move_volume_slider(p);
}
</pre>
One may think that it's enough to set a global ``current volume''
variable in the callback and to return it in the getter. This can't
work because the following property is required:
<pre>
x == SIO_TO_PCT(PCT_TO_SIO(x)) /* for all x */
y == PCT_TO_SIO(SIO_TO_PCT(y)) /* for all y */
</pre>
So it may lead to various weird effects like the cursor stuttering
around a given position, or ``+/- volume'' keyboard shortcuts not
working.
The correct implementation is to use feedback as in the above section.
If that's not possible, a fake getter can be implemented as follows:
<pre>
unsigned current_pct;
void
cb(void *addr, unsigned vol)
{
if (vol != PCT_TO_SIO(current_pct))
current_pct = SIO_TO_PCT(vol);
}
unsigned
getvol(int p)
{
return current_pct;
}
</pre>
<h2><a name="section_7">7 Pausing and resuming</a></h2>
<p>
Pause and resume functions do not exist, because it's hard to properly
implement on <b>any</b> hardware.
If the pause feature is required, it's easier to stop the
stream with sio_stop(3) and to later restart it with sio_start(3).
<p>
Certain programs expect a pause-resume cycle to not change the
amount of buffered data.
If so, the "resume" function must play the same amount of silence as the
amount of data the buffer contained when the "pause" function was called.
<p>
<b>Update : </b>Doing nothing would also work, but only in few cases.
If you just stop providing data to play, the stream will underrun and stop
automatically.
Once data is available again, the stream will resume automatically.
However, this abuse of the xrun mechanism is not desirable for two reasons:
<ul>
<li>The device will still be processing data (silence)
and will waste CPU time (which consumes more energy from laptop batteries).
<li>This doesn't work if sndiod(8) is used and the subdevice is controlled
by MMC.
Indeed, sndiod(8) will try to resynchronize after the underrun and will
drop a huge amount of samples, corresponding to the duration of the pause.
</ul>
<h2><a name="section_8">8 Using sndio in multi-threaded programs</a></h2>
<p>
The sndio library can be safely used in multi-threaded programs as long
as all calls to function using the same handle are serialized.
This is achieved either with locks or by simply running all sndio
related bits in the same thread.
In any case, using multiple threads to handle audio I/O buys nothing since
the process is I/O bound.
<h2><a name="section_9">9 Windows-style callbacks</a></h2>
<p>
Certain programs expect to register a callback that will be invoked
automatically by the audio subsystem whenever the play buffer must be
filled.
For instance, Windows, jack and portaudio APIs use such semantics;
callbacks are tipically called by a real-time thread or in an interrupt
context.
This approach is equivalent to the read/write based approach,
which is widespread on Unix.
Consider the following callback-style pseudo-code:
<pre>
void
play_cb(void *buf, size_t buflen)
{
/* fill buf with data to play */
}
int
main(void)
{
register_audio_callback(play_cb);
...
wait_forever();
}
</pre>
It could be rewritten using read/write style semantics:
<pre>
void
play_cb(void *buf, size_t buflen)
{
/* fill buf with data to play */
}
int
main(void)
{
unsigned char *buf;
unsigned buflen = par.round;
...
for (;;) {
play_cb(buf, buflen);
sio_write(hdl, buf, buflen);
}
}
</pre>
there's no fundamental difference.
In other words, any callback style API could be exposed using sndio.
The only remaining problem is where to put the sndio loop.
<p>
If the program is single-threaded, then it uses a poll(2)-based event
loop, in which case non-blocking I/O should be used and the sndio bits
should be hooked somewhere in the poll(2) loop.
However, such programs probably come from the Unix world and don't use a
callback-style API.
<p>
If the program is multi-threaded, then it is simpler to spawn
a thread and run the simple loop from above in it.
The thread could be spawned when sio_start(3) is called and
terminated when sio_stop(3) is called; if so, the thread contains
real-time code paths only, and its scheduling priority could be cranked.
<p>
Multi-threaded programs use locks for synchronization, and
we don't want a thread to sleep while holding a lock.
To avoid holding a lock while a blocking sio_write(3) call is sleeping,
one can use non-blocking I/O and sleep in poll(2) without holding the
lock. In other words, sio_write(2) could be expanded as follows:
<pre>
unsigned char *p = buf;
struct pollfds pfds[MAXFDS];
...
pthread_mutex_lock(&hdl_mtx);
...
for (;;) {
if (p - buf == buflen) {
play_cb(buf, buflen);
p = buf;
}
n = sio_pollfds(hdl, pfds);
pthread_mutex_unlock(&hdl_mtx);
if (n > 0 && poll(pfds, n, -1) < 0) {
pthread_mutex_lock(&hdl_mtx);
continue;
}
pthread_mutex_lock(&hdl_mtx);
if (sio_revents(hdl, pfds) & POLLOUT) {
n = sio_write(hdl, p, buflen - (p - buf));
p += n;
}
}
</pre>
<h2><a name="section_10">10 Pitfalls</a></h2>
<h3><a name="section_10_1">10.1 Playback and record aren't independent</a></h3>
<p>
If for any reason a full-duplex program stops consuming recorded
data, there's a buffer overrun and recording stops. But since playback
and record direction are synchronous, this will also stop playback.
For instance, waiting for playback to drain without consuming recorded
data will never complete, because the record direction will pause
the stream because of the overrun. Deadlock occurs.
<h3><a name="section_10_2">10.2 Using appbufsz instead of bufsz</a></h3>
<p>
The ``appbufsz'' parameter is the size of the buffer the application
is responsible for keeping non-empty (playback) or non-full (record).
It should never be used for latency or buffer usage calculations.
<p>
The ``bufsz'' parameter is read-only and gives the total buffering
between the application and Joe's ears, i.e. it's the actual latency.
It takes into account any buffering including uncontrolled buffering
of network sockets.
<h3><a name="section_10_3">10.3 How many bytes to store a 24-bit sample</a></h3>
<p>
Short answer: four. Hardware, as most of the software, stores 24-bit
samples in 4-byte words. This format is often referred to as ``s24le''
or ``s24be'', and it's the default when 24-bit encodings are requested.
<p>
However, that's not always the case: .wav and .aiff files store 24-bit
samples in 3-byte words to save space. This encoding is often
referred to as ``s24le3'' or ``s24be3''.
If a program just reads and plays such files without any processing,
it's likely it will try to send the file contents on the audio stream
as-is. If so, the parameters should be set as follows:
<pre>
par.bits = 24;
par.bps = 3;
</pre>
<h3><a name="section_10_4">10.4 poll(2) not called fast enough</a></h3>
<p>
A (wrong) program may use the following approach. Consider the
following function to wait for the play buffer to become ready:
<pre>
void
wait_ready(void)
{
/*
* wait buffer to be consumed, sleep not to hog the CPU
*/
while (bufused > threshold)
usleep(5);
}
</pre>
where the ``bufused'' variable is updated asynchronously by the
callback set with sio_onmove(3). Suppose it's then called as
follows:
<pre>
for (;;) {
prepare_data(some_data);
wait_ready();
sio_write(hdl, some_data, count);
}
</pre>
This will deadlock. The callback is invoked from sio_write(3), but
sio_write(3) is not called until ``bufused'' is updated by the
callback. The correct implementation uses poll(2) as follows; it's
also more efficient:
<pre>
void
wait_ready(void)
{
int nfds, revents;
struct pollfd pfds[1];
do {
nfds = sio_pollfd(hdl, pfds, POLLOUT);
if (nfds > 0) {
if (poll(pfds, nfds, -1) < 0)
err(1, "poll failed");
}
revents = sio_revents(hdl, pfds);
} while (!(revents & POLLOUT));
}
</pre>
<h2><a name="section_11">11 Glossary</a></h2>
<p>
<dl>
<dt>channel
<dd>
that's a mono signal. Multiple channels form an audio stream. For example,
a stereo stream has two channels: left and right. Channels are
identified by small integers rather than names; so ``channel 0'' means
the ``left channel''.
<p>
channels numbers start from zero and are ordered as follows:
<p align="center">
<table>
<tr>
<th>channel number
<th>physical meaning
<tr>
<td>0
<td>main left
<tr>