1 /* Invisible Vector Library
2 * coded by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
3 * Understanding is not required. Only obedience.
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, version 3 of the License ONLY.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 module iv
.follin
.engine
/*is aliced*/;
19 version = follin_use_spinrw
;
20 //version = follin_prefer_alsa_plug;
21 //version = follin_threads_debug;
22 //version = follin_wait_debug;
23 //version = follin_debug_resampler_type;
26 import iv
.follin
.exception
;
29 // ////////////////////////////////////////////////////////////////////////// //
32 import core.sys.posix.signal : siginfo_t, sigaction_t;
33 private extern(C) void handleSignalZZ (int signum, siginfo_t* info, void* contextPtr) nothrow {
34 import core.stdc.stdio : fprintf, stderr;
35 import core.stdc.stdlib : abort;
36 fprintf(stderr, "\bFUUUUUUU....\n");
39 private __gshared sigaction_t old_sigaction;
41 shared static this () {
42 //import etc.linux.memoryerror;
43 //registerMemoryErrorHandler();
44 import core.sys.posix.signal;
46 action.sa_sigaction = &handleSignalZZ;
47 action.sa_flags = SA_SIGINFO;
48 auto oldptr = &old_sigaction;
49 sigaction(SIGSEGV, &action, oldptr);
54 // ////////////////////////////////////////////////////////////////////////// //
56 version(D_PIC
) {} else {
57 version = follin_use_sse
;
58 version = follin_use_sse2
;
62 import iv
.follin
.utils
;
65 // ////////////////////////////////////////////////////////////////////////// //
66 // throws FollinException on error
69 version(follin_prefer_alsa_plug
) {
70 static immutable string
[2] devnames
= ["plug:default", "default"];
72 static immutable string
[2] devnames
= ["default", "plug:default"];
74 if (!cas(&initialized
, false, true)) throw new FollinException("double initialization");
75 foreach (string name
; devnames
) {
77 sndInit(name
.ptr
, samplerate
);
79 atomicStore(sndPaused
, false);
80 atomicStore(sndWantShutdown
, false);
81 atomicStore(sndSafeToShutdown0
, false);
82 atomicStore(sndSafeToShutdown1
, false);
83 //{ import core.stdc.stdio; printf("pmin=%u; pmax=%u; pdef=%u\n", Thread.PRIORITY_MIN, Thread.PRIORITY_MAX, Thread.PRIORITY_DEFAULT); }
84 // 2.066 compatibility
85 sndThread
= new Thread(&sndPlayTreadFunc
);
87 sndMixThread
= new Thread(&sndMixTreadFunc
);
90 } catch (FollinException e
) {
91 import core
.stdc
.stdio
: fprintf
, stderr
;
92 fprintf(stderr
, "Follin: '%s' error: %.*s", name
.ptr
, cast(int)e
.msg
.length
, cast(const(char)*)e
.msg
.ptr
);
95 atomicStore(initialized
, false);
96 throw new FollinException("can't initialize audio");
99 void tflDeinit () nothrow @trusted /*@nogc*/ { sndEngineDeinit(); }
102 @property nothrow @trusted @nogc bool tflInitialized () { static if (__VERSION__
> 2067) pragma(inline
, true); return atomicLoad(initialized
); }
104 @property pure nothrow @safe @nogc {
105 bool tflStereo () { static if (__VERSION__
> 2067) pragma(inline
, true); return (numchans
== 2); }
106 uint tflBytesPerSample () { static if (__VERSION__
> 2067) pragma(inline
, true); return 2; }
109 @property nothrow @trusted @nogc {
110 uint tflSampleRate () { static if (__VERSION__
> 2067) pragma(inline
, true); return realSampleRate
; }
111 uint tflLatency () { static if (__VERSION__
> 2067) pragma(inline
, true); return latency
*bufcount
; } // in milliseconds
112 uint tflActiveChannels () { static if (__VERSION__
> 2067) pragma(inline
, true); return atomicLoad(/*sndActiveChanCount*/firstFreeChan
); }
114 bool tflPaused () { static if (__VERSION__
> 2067) pragma(inline
, true); return atomicLoad(sndPaused
); }
115 void tflPaused (bool v
) { static if (__VERSION__
> 2067) pragma(inline
, true); atomicStore(sndPaused
, v
); }
117 ubyte tflMasterVolume () {
118 static if (__VERSION__
> 2067) pragma(inline
, true);
119 auto l
= atomicLoad(sndMasterVolume
);
120 auto r
= (l
>>8)&0xff;
121 return cast(ubyte)(l
> r ? l
: r
);
123 void tflMasterVolume (ubyte v
) { static if (__VERSION__
> 2067) pragma(inline
, true); atomicStore(sndMasterVolume
, cast(ushort)(v
<<8|v
)); }
125 ubyte tflMasterVolumeL () { static if (__VERSION__
> 2067) pragma(inline
, true); return atomicLoad(sndMasterVolume
)&0xff; }
126 ubyte tflMasterVolumeR () { static if (__VERSION__
> 2067) pragma(inline
, true); return (atomicLoad(sndMasterVolume
)>>8)&0xff; }
127 void tflMasterVolumeL (ubyte v
) { static if (__VERSION__
> 2067) pragma(inline
, true); atomicStore(sndMasterVolume
, cast(ushort)((atomicLoad(sndMasterVolume
)&0xff00)|v
)); }
128 void tflMasterVolumeR (ubyte v
) { static if (__VERSION__
> 2067) pragma(inline
, true); atomicStore(sndMasterVolume
, cast(ushort)((atomicLoad(sndMasterVolume
)&0x00ff)|
(v
<<8))); }
131 // callback is called inside mixing thread and lock
132 auto tflS16Callback () { return sndS16CB
; }
133 void tflS16Callback (void delegate (short[] buf
) nothrow @nogc dg
) { sndS16CB
= dg
; }
137 // ////////////////////////////////////////////////////////////////////////// //
138 // channel management
139 // note that any method can be called while sound engine is locked, and from arbitrary thread
141 // resampling quality
143 enum QualityMin
= -1; // try cubic upsampler
144 enum QualityMax
= 10;
145 enum QualityMusic
= 8;
146 enum QualitySfx
= -1; // default
148 // volumes for each channel
151 uint sampleRate
= 44100;
155 final @property ubyte volume () pure const nothrow @safe @nogc { return (volL
> volR ? volL
: volR
); }
156 final @property void volume (ubyte v
) nothrow @safe @nogc { volL
= volR
= v
; }
158 // all of the above will be read before `fillFrames`
159 // if channel is paused, `fillFrames` will not be called
161 // override this if you can provide floating buffer
162 // Follin will provide `tmpbuf` of the same size as `buf`
163 // return number of *frames* (not samples!) written
164 // 0 means "this channel is no more"
165 // note that this function SHOULD NOT ALLOCATE, despite commented out @nogc
166 uint fillFrames (float[] buf
) nothrow /*@nogc*/ { return 0; }
168 // called when the channel is discarded (due to being `done` or by replacing with another channel)
169 // note that sound engine is locked, so you can't call `tflAddChan()` here
170 // also, this can (and will) be called from different threads
171 void discarded () nothrow @nogc {}
173 // can be overriden by channel if it knows it's length
174 // this is purely for infomation purposes, Follin will not use this for anything
175 // return -1 for "unknown", or total channel duration in milliseconds
176 long totalMsecs () { return -1; }
180 // default priority; channel with lesser priority wins
181 enum TFLdefault
= 1000;
182 enum TFLplayer
= 500;
183 enum TFLmusic
= 100; // music should always play
185 // returns `false` if there's no room for new channel (and it's not added)
186 bool tflAddChannel (const(char)[] name
, TflChannel chan
, uint prio
=TFLdefault
, TflChannel
.Quality q
=TflChannel
.QualitySfx
) { static if (__VERSION__
> 2067) pragma(inline
, true); return sndAddChan(name
, chan
, prio
, q
); }
187 bool tflKillChannel (const(char)[] name
) { /*static if (__VERSION__ > 2067) pragma(inline, true);*/ return sndKillChan(name
); }
188 bool tflIsChannelAlive (const(char)[] name
) nothrow @trusted /*@nogc*/ { static if (__VERSION__
> 2067) pragma(inline
, true); return sndIsChanAlive(name
); }
189 TflChannel
tflChannelObject (const(char)[] name
) nothrow @trusted /*@nogc*/ { static if (__VERSION__
> 2067) pragma(inline
, true); return sndGetChanObj(name
); }
190 uint tflChannelPlayTimeMsec (const(char)[] name
) nothrow @trusted /*@nogc*/ { static if (__VERSION__
> 2067) pragma(inline
, true); return sndGetPlayTimeMsec(name
); }
192 // ////////////////////////////////////////////////////////////////////////// //
195 import core
.sync
.condition
;
196 import core
.sync
.rwmutex
;
197 import core
.sync
.mutex
;
200 import iv
.follin
.drivers
;
201 import iv
.follin
.ftrick
;
202 import iv
.follin
.hash
;
203 import iv
.follin
.resampler
;
204 import iv
.follin
.sdata
;
205 version(follin_use_spinrw
) import iv
.follin
.rwlock
;
208 enum samplerate
= 44100;
212 //enum latency = 200; // milliseconds
213 //static assert(1000%latency == 0);
214 shared bool initialized
= false;
215 shared bool sndWantShutdown
= false;
216 shared bool sndSafeToShutdown0
= false, sndSafeToShutdown1
= false;
217 //shared uint sndActiveChanCount;
218 __gshared
void delegate (short[] buf
) nothrow @nogc sndS16CB
;
219 __gshared
short* sndbufMem
= null; // chunk of memory for `sndbufptr`
220 __gshared
float* sndrsbufMem
= null; // chunk of memory for `sndrsbufptr`
221 __gshared
float* sndrsbufptr
= null; // aligned on 16 bytes for sse
222 __gshared
float* tmprsbufMem
= null; // chunk of memory for `tmprsbufptr`
223 __gshared
float* tmprsbufptr
= null; // here we'll collect floating point samples; aligned on 16 bytes for sse
224 __gshared
float* tmpmonobufMem
= null; // chunk of memory for `tmpmonobufptr`
225 __gshared
float* tmpmonobufptr
= null; // here we'll collect mono samples; aligned on 16 bytes for sse
226 __gshared Thread sndThread
= null, sndMixThread
= null;
227 shared ushort sndMasterVolume
= 0xffff;
228 version(follin_use_spinrw
) {
229 shared TflRWLock sndMutexChanRW
;
231 __gshared ReadWriteMutex sndMutexChanRW
;
233 __gshared Mutex mutexCondMixer
;
234 __gshared Condition condMixerWait
;
236 shared static this () {
237 mutexCondMixer
= new Mutex();
238 condMixerWait
= new Condition(mutexCondMixer
);
239 version(follin_use_spinrw
) {} else sndMutexChanRW
= new ReadWriteMutex(ReadWriteMutex
.Policy
.PREFER_WRITERS
);
243 // ////////////////////////////////////////////////////////////////////////// //
244 private T
* alignTo16(T
) (T
* ptr
) {
245 pragma(inline
, true);
246 if ((cast(usize
)ptr
)&0x0f) ptr
= cast(T
*)(((cast(usize
)ptr
)|
0x0f)+1);
251 void sndEngineInit () {
252 static T
* realloc(T
) (ref T
* ptr
, uint len
) {
253 import core
.stdc
.stdlib
: realloc
;
254 ptr
= cast(T
*)realloc(ptr
, len
*T
.sizeof
);
255 if (ptr
is null) assert(0, "Follin: out of memory");
256 return alignTo16(ptr
);
258 sndSamplesSize
*= numchans
; // frames to samples
259 // buffers for final mixed sound
260 sndbufptr
= cast(short**)realloc(sndbufptr
, (short*).sizeof
*bufcount
);
261 if (sndbufptr
is null) assert(0, "Follin: out of memory");
262 sndbufptr
[0] = realloc(sndbufMem
, (sndSamplesSize
+128)*bufcount
);
263 //{ import std.stdio; writefln("0x%08x 0x%08x 0x%08x", cast(uint)sndbufMem, cast(uint)sndbufptr[0], cast(uint)sndbufptr[1]); }
264 foreach (immutable idx
; 1..bufcount
) sndbufptr
[idx
] = alignTo16(sndbufptr
[idx
-1]+sndSamplesSize
+8);
265 // buffer to collect stereo samples (float)
266 tmprsbufptr
= realloc(tmprsbufMem
, sndSamplesSize
+128);
267 // buffer to collect mono samples (float)
268 tmpmonobufptr
= realloc(tmpmonobufMem
, sndSamplesSize
+128);
269 // buffer to mix samples (float)
270 sndrsbufptr
= realloc(sndrsbufMem
, sndSamplesSize
+128);
272 foreach (ref ch
; chans
) {
273 import core
.stdc
.stdlib
: malloc
;
274 ch
.bufptr
= realloc(ch
.bufMem
, sndSamplesSize
+128);
276 if (!ch
.srb
.inited
) {
279 ch
.srb
.setup(numchans
, 44100, 48000, ch
.lastquality
);
282 ch
.prevsrate
= ch
.lastsrate
= 44100;
286 import core.stdc.stdlib : free;
287 foreach (ref ch; chans) {
288 if (ch.bufMem !is null) { free(ch.bufMem); ch.bufMem = null; }
295 // ////////////////////////////////////////////////////////////////////////// //
297 uint prio
; // channel priority
299 usize namelen
; // 0: this channel is free
300 char[128] name
; // no longer than this
301 TflChannel chan
; // can be `null` if channel callback is done, but something is still playing
304 // buffers are always of `sndSamplesSize` size
305 float* bufMem
; // chunk of memory for `buf`
306 float* bufptr
; // frame buffer, aligned to 16 bytes
307 uint bufpos
; // current position to write in `tmpbuf`/`buf`
308 ubyte lastvolL
, lastvolR
; // latest volume
309 int lastquality
= 666; // <0: try cubic
311 uint lastsrate
, prevsrate
; // last sampling rate
312 ulong genFrames
; // generated, but not yet consumed frames
315 @disable this (this); // no copies
319 __gshared Channel
[maxchans
] chans
;
320 shared ulong chanid
= 0;
321 shared uint firstFreeChan
= 0;
322 __gshared
uint sndFrameId
;
325 void packChannels () nothrow @trusted @nogc {
326 import core
.stdc
.string
: memcpy
;
328 __gshared
ubyte[Channel
.srb
.sizeof
> Channel
.cub
.sizeof ? Channel
.srb
.sizeof
: Channel
.cub
.sizeof
] srbMoveBuf
= void;
333 while (freeIdx
< firstFreeChan
&& chans
.ptr
[freeIdx
].namelen
!= 0) ++freeIdx
;
334 if (freeIdx
>= firstFreeChan
) return; // nothing to do
337 while (idx
< firstFreeChan
&& chans
.ptr
[idx
].namelen
== 0) ++idx
;
338 if (idx
>= firstFreeChan
) { firstFreeChan
= freeIdx
; return; } // done
339 // now move used channel up
340 auto sc
= &chans
.ptr
[idx
];
341 auto dc
= &chans
.ptr
[freeIdx
];
343 sc
.namehash
= dc
.namehash
;
344 sc
.namelen
= dc
.namelen
;
347 // now tricky part: swap SpeexResampler
348 memcpy(srbMoveBuf
.ptr
, &sc
.srb
, sc
.srb
.sizeof
);
349 memcpy(&sc
.srb
, &dc
.srb
, sc
.srb
.sizeof
);
350 memcpy(&dc
.srb
, srbMoveBuf
.ptr
, sc
.srb
.sizeof
);
351 // now tricky part: swap CubicUpsampler
352 memcpy(srbMoveBuf
.ptr
, &sc
.cub
, sc
.cub
.sizeof
);
353 memcpy(&sc
.cub
, &dc
.cub
, sc
.cub
.sizeof
);
354 memcpy(&dc
.cub
, srbMoveBuf
.ptr
, sc
.cub
.sizeof
);
362 Channel
* sndFindChanByName (const(char)[] name
) nothrow @trusted @nogc {
363 if (name
.length
== 0 || name
.length
> Channel
.name
.length
) return null;
364 auto hash
= hashBuffer(name
.ptr
, name
.length
);
365 foreach (immutable idx
; 0..chans
.length
) {
366 auto ch
= chans
.ptr
+idx
;
367 if (ch
.namelen
== 0) break;
368 if (ch
.namehash
== hash
&& ch
.namelen
== name
.length
&& ch
.name
[0..ch
.namelen
] == name
) return ch
;
374 bool sndIsChanAlive (const(char)[] name
) nothrow @trusted /*@nogc*/ {
375 version(follin_use_spinrw
) {
376 sndMutexChanRW
.readLock();
377 auto ch
= sndFindChanByName(name
);
378 sndMutexChanRW
.readUnlock();
379 return (ch
!is null);
381 synchronized (sndMutexChanRW
.reader
) {
382 if (auto ch
= sndFindChanByName(name
)) return true;
389 TflChannel
sndGetChanObj (const(char)[] name
) nothrow @trusted /*@nogc*/ {
390 version(follin_use_spinrw
) {
391 sndMutexChanRW
.readLock();
392 auto ch
= sndFindChanByName(name
);
393 TflChannel res
= (ch
!is null ? ch
.chan
: null);
394 sndMutexChanRW
.readUnlock();
397 synchronized (sndMutexChanRW
.reader
) {
398 if (auto ch
= sndFindChanByName(name
)) return ch
.chan
;
405 uint sndGetPlayTimeMsec (const(char)[] name
) nothrow @trusted /*@nogc*/ {
406 version(follin_use_spinrw
) {
407 sndMutexChanRW
.readLock();
408 if (auto ch
= sndFindChanByName(name
)) {
409 auto res
= cast(uint)(ch
.playedFrames
*1000/realSampleRate
);
410 sndMutexChanRW
.readUnlock();
413 sndMutexChanRW
.readUnlock();
416 synchronized (sndMutexChanRW
.reader
) {
417 if (auto ch
= sndFindChanByName(name
)) return cast(uint)(ch
.playedFrames
*1000/realSampleRate
);
424 bool sndKillChan (const(char)[] name
) {
425 static bool doIt() (usize hash
, const(char)[] name
) {
427 foreach (immutable idx
; 0..chans
.length
) {
428 if (ch
.namelen
== 0) break;
429 if (ch
.namehash
== hash
&& ch
.namelen
== name
.length
&& ch
.name
[0..ch
.namelen
] == name
) {
431 auto ochan
= ch
.chan
;
436 if (ochan
!is null) ochan
.discarded();
445 if (name
.length
== 0 || name
.length
> Channel
.name
.length
) return false;
446 auto hash
= hashBuffer(name
.ptr
, name
.length
);
447 version(follin_use_spinrw
) {
448 sndMutexChanRW
.writeLock();
449 auto res
= doIt(hash
, name
);
450 sndMutexChanRW
.writeUnlock();
453 synchronized (sndMutexChanRW
.writer
) return doIt(hash
, name
);
458 bool sndAddChan (const(char)[] name
, TflChannel chan
, uint prio
, TflChannel
.Quality q
) {
460 // build name if there's none
461 if (name
.length
== 0) {
462 if (chan
is null) return false;
463 usize pos
= tmpname
.length
;
464 ulong num
= atomicOp
!"+="(chanid
, 1);
465 do { tmpname
[--pos
] = cast(char)('0'+num
%10); } while ((num
/= 10) != 0);
466 enum pfx
= "uninspiring channel #";
468 tmpname
[pos
..pos
+pfx
.length
] = pfx
[];
469 name
= tmpname
[pos
..$];
470 } else if (name
.length
> Channel
.name
.length
) {
471 //throw FollinException("channel name too long: '"~name.idup~"'");
474 auto hash
= hashBuffer(name
.ptr
, name
.length
);
476 static struct VictimInfo
{
479 int state
= int.max
; // bufpos, or `int.max-1` for paused
481 static T
max(T
) (T a
, T b
) { return (a
> b ? a
: b
); }
482 static T
min(T
) (T a
, T b
) { return (a
< b ? a
: b
); }
484 void fixVictim(string mode
) (ref Channel ch
, uint prio
, usize curidx
) nothrow @trusted @nogc {
485 static assert(mode
== "same" || mode
== "lower");
486 static if (mode
== "same") {
487 if (ch
.prio
!= prio
) return;
489 if (ch
.prio
<= prio
) return;
490 if (idx
>= 0 && chans
.ptr
[idx
].prio
> ch
.prio
) {
491 // we have a victim with better priority, but we'll prefer "going to die" to paused one
492 if (chans
.ptr
[idx
].chan
is null || ch
.chan
!is null) return;
495 if (ch
.chan
is null) {
496 // this channel is going to die
497 if (ch
.bufpos
< state ||
max(ch
.lastvolL
, ch
.lastvolR
) < vol
) {
498 // this channel is better victim: has less data left, or has lower volume
499 idx
= cast(int)curidx
;
501 vol
= max(ch
.lastvolL
, ch
.lastvolR
);
503 } else if (ch
.chan
.paused
) {
504 // this channel is paused
505 if (state
== int.max ||
(state
== int.max
-1 && max(ch
.lastvolL
, ch
.lastvolR
) < vol
)) {
506 // this channel is better victim: either there is no victim found yet, or has paused victim with lower volume
507 idx
= cast(int)curidx
;
509 vol
= max(ch
.lastvolL
, ch
.lastvolR
);
515 VictimInfo lower
, same
;
519 // for lowest prio: prefer finished channel, then paused channel
520 foreach (immutable idx
; 0..chans
.length
) {
521 auto ch
= chans
.ptr
+idx
;
522 if (ch
.namelen
== 0) break; // last channel
523 if (ch
.namehash
== hash
&& ch
.namelen
== name
.length
&& ch
.name
[0..ch
.namelen
] == name
) {
524 replaceIdx
= cast(int)idx
;
527 lower
.fixVictim
!"lower"(*ch
, prio
, idx
);
528 same
.fixVictim
!"same"(*ch
, prio
, idx
);
531 //{ import core.stdc.stdio; printf("replaceIdx(0)=%d; firstFreeChan=%u\n", replaceIdx, firstFreeChan); }
533 if (replaceIdx
< 0) {
534 if (chan
is null) return false;
535 if (lower
.idx
>= 0 && lower
.state
< int.max
-1) {
537 replaceIdx
= lower
.idx
;
538 } else if (same
.idx
>= 0 && same
.state
< int.max
-1) {
540 replaceIdx
= same
.idx
;
541 } else if (lower
.idx
>= 0) {
543 replaceIdx
= lower
.idx
;
546 replaceIdx
= same
.idx
;
548 if (replaceIdx
< 0) {
549 if (firstFreeChan
>= chans
.length
) return false; // alas
550 replaceIdx
= firstFreeChan
;
554 //{ import core.stdc.stdio; printf("replaceIdx(1)=%d; firstFreeChan=%u\n", replaceIdx, firstFreeChan); }
557 auto ch
= &chans
.ptr
[replaceIdx
];
559 // fix resampling ratio
560 if (chan
!is null && ch
.chan
!is chan
) {
561 auto srate
= chan
.sampleRate
;
562 ch
.lastsrate
= (srate
< 1024 || srate
> 96000 ?
0 : srate
);
563 if (ch
.lastsrate
!= 0) {
565 if (ch
.prevsrate
!= ch
.lastsrate
&& ch
.lastsrate
!= realSampleRate
) {
566 if (ch
.srb
.setRate(ch
.lastsrate
, realSampleRate
)) ch
.lastsrate
= 0;
569 ch
.prevsrate
= ch
.lastsrate
;
570 if (ch
.lastsrate
== 0) chan
= null;
573 if (ch
.chan
!is chan
) {
574 auto ochan
= ch
.chan
;
579 if (ochan
!is null) ochan
.discarded();
580 ch
.cub
.reset(); // it is fast
584 //if (ch.chan is null) ch.srb.reset();
585 if (q
< -1) q
= -1; else if (q
> 10) q
= 10;
586 if (ch
.lastquality
!= q
) {
587 //{ import core.stdc.stdio; printf("changing quality from %u to %u\n", ch.lastquality, q); }
590 ch
.srb
.setQuality(q
);
593 ch
.srb
.setup(numchans
, 44100, 48000, q
);
596 if (ch
.prevsrate
!= ch
.lastsrate
) {
597 if (ch
.srb
.setRate(ch
.lastsrate
, realSampleRate
)) {
598 ch
.prevsrate
= ch
.lastsrate
= 0;
599 auto ochan
= ch
.chan
;
602 if (ochan
!is null) ochan
.discarded();
603 ch
.namelen
= 0; // "it's free" flag
608 if (ch
.lastquality
< 0) ch
.useCubic
= ch
.cub
.setup(cast(float)ch
.lastsrate
/cast(float)realSampleRate
); // don't use cubic
609 version(follin_debug_resampler_type
) if (ch
.useCubic
) { import core
.stdc
.stdio
; printf("*** using cubic upsampler\n"); }
610 ch
.prevsrate
= ch
.lastsrate
;
611 if (ch
.bufpos
== 0) ch
.lastvolL
= ch
.lastvolR
= 0; // so if we won't even had a chance to play it, it can be replaced
614 ch
.namelen
= name
.length
;
615 ch
.name
[0..name
.length
] = name
;
617 if (replaceIdx
>= firstFreeChan
) {
618 firstFreeChan
= replaceIdx
+1;
622 ch
.namelen
= 0; // "it's free" flag
628 version(follin_use_spinrw
) {
629 sndMutexChanRW
.writeLock();
631 sndMutexChanRW
.writeUnlock();
634 synchronized (sndMutexChanRW
.writer
) return doIt();
639 // ////////////////////////////////////////////////////////////////////////// //
640 bool sndGenerateBuffer () {
641 //version(follin_use_sse) align(64) __gshared float[4] zeroes = 0; // dmd cannot into such aligns, alas
642 version(follin_use_sse
) {
643 align(64) __gshared
float[4+32] zeroesBuf
= 0; // dmd cannot into such aligns, alas
644 __gshared
uint zeroesptr
= 0;
645 if (zeroesptr
== 0) {
646 zeroesptr
= cast(uint)zeroesBuf
.ptr
;
647 if (zeroesptr
&0x3f) zeroesptr
= (zeroesptr|
0x3f)+1;
649 assert((zeroesptr
&0x3f) == 0, "wtf?!");
651 //version(follin_use_sse) assert(((cast(usize)zeroes.ptr)&0x3f) == 0, "wtf?!");
653 static void killChan() (Channel
* ch
, ref bool channelsChanged
) {
655 channelsChanged
= true;
670 SpeexResampler
.Error err
;
671 SpeexResampler
.Data srbdata
= void;
672 bool channelsChanged
= false;
673 auto buf2fill
= atomicLoad(sndbufToFill
);
674 atomicStore(sndbufFillingNow
, true);
675 bool wasAtLeastOne
= false;
678 if (firstFreeChan
> 0) {
679 immutable bufsz
= sndSamplesSize
;
680 // use SSE to clear buffer, if we can
681 version(follin_use_sse
) {
682 asm nothrow @safe @nogc {
683 mov EAX
,[sndrsbufptr
];
684 // ECX = (sndSamplesSize+3)/4
685 mov ECX
,[sndSamplesSize
];
688 //mov EBX,offsetof zeroes[0];
690 //movntdqa XMM0,[EBX]; // non-temporal, don't bring zeroes to cache (sse4.1)
694 movaps [EAX
],XMM0
; // dest is always aligned
700 sndrsbufptr
[0..sndSamplesSize
] = 0.0f;
702 //{ import core.stdc.stdio; printf("filling buffer %u\n", buf2fill); }
703 foreach (immutable cidx
; 0..chans
.length
) {
704 auto ch
= chans
.ptr
+cidx
;
705 if (ch
.namelen
== 0) break; // last channel
706 uint rspos
= 0; // current position in `sndrsbufptr`
707 chmixloop
: while (rspos
< bufsz
) {
708 // try to get as much data from the channel as we can
709 if (ch
.bufpos
== 0 && (ch
.chan
!is null && !ch
.chan
.paused
) && ch
.lastsrate
!= ch
.chan
.sampleRate
) {
710 auto srate
= ch
.chan
.sampleRate
;
711 ch
.lastsrate
= srate
;
712 if (srate
< 1024 || srate
> 96000) {
713 ch
.lastsrate
= srate
= 0;
714 } else if (srate
!= ch
.prevsrate
&& srate
!= realSampleRate
) {
715 if (ch
.srb
.setRate(srate
, realSampleRate
)) ch
.lastsrate
= srate
= 0;
717 ch
.prevsrate
= ch
.lastsrate
;
718 if (!srate
) { killChan(ch
, channelsChanged
); break chmixloop
; } // something is wrong with this channel, kill it
719 if (ch
.lastquality
< 0) ch
.useCubic
= ch
.cub
.setup(cast(float)srate
/cast(float)realSampleRate
); // don't use cubic
720 version(follin_debug_resampler_type
) { if (ch
.useCubic
) { import core
.stdc
.stdio
; printf("*** using cubic upsampler\n"); } }
722 //{ import core.stdc.stdio; printf("wanted %u frames (has %u frames)\n", (bufsz-ch.bufpos)/2, ch.bufpos/2); }
723 while (ch
.bufpos
< bufsz
&& (ch
.chan
!is null && !ch
.chan
.paused
) && ch
.lastsrate
== ch
.chan
.sampleRate
) {
724 // fix last known volume
725 ch
.lastvolL
= ch
.chan
.volL
;
726 ch
.lastvolR
= ch
.chan
.volR
;
727 // fix last known sample rate
729 auto len
= (bufsz
-ch
.bufpos
)/2; // frames
730 if (ch
.chan
.stereo
) {
732 fblen
= ch
.chan
.fillFrames(ch
.bufptr
[ch
.bufpos
..bufsz
]);
733 if (fblen
> len
) fblen
= 0; // something is very wrong with this channel
736 fblen
= ch
.chan
.fillFrames(tmpmonobufptr
[0..len
]);
737 if (fblen
> len
) fblen
= 0; // something is very wrong with this channel
739 auto s
= tmpmonobufptr
;
740 auto d
= ch
.bufptr
+ch
.bufpos
;
741 foreach (immutable _
; 0..fblen
) {
746 //{ import core.stdc.stdio; printf("trying to get %u frames, got %u frames...\n", (bufsz-ch.bufpos)/2, fblen); }
747 if (fblen
== 0) { killChan(ch
, channelsChanged
); break; }
749 auto bptr
= ch
.bufptr
+ch
.bufpos
;
750 ch
.bufpos
+= fblen
*2; // frames to samples
751 if ((ch
.lastvolL|ch
.lastvolR
) == 0) {
753 version(follin_use_sse
) {
754 asm nothrow @safe @nogc {
756 //mov EBX,offsetof zeroes[0];
758 //movntdqa XMM0,[EBX]; // non-temporal (sse4.1)
762 // is buffer aligned?
763 // process floats one-by-one if not
767 // using `movsd` brings some penalty here, but meh...
768 movsd [EAX
],XMM0
; // store two floats (single double)
771 jnz zerovol_unaligned
;
787 //ch.bufptr[ch.bufpos..bufsz] = 0.0f;
788 bptr
[0..fblen
*2] = 0;
791 version(follin_use_sse
) {
792 if (ch
.lastvolL
!= 255 || ch
.lastvolR
!= 255) {
793 align(64) __gshared
float[4] mul = void;
794 mul[0] = mul[2] = (1.0f/255.0f)*cast(float)ch
.lastvolL
;
795 mul[1] = mul[3] = (1.0f/255.0f)*cast(float)ch
.lastvolR
;
796 asm nothrow @safe @nogc {
798 mov EBX
,offsetof
mul[0];
799 //movntdqa XMM1,[EBX]; // non-temporal, don't bring volumes to cache (sse4.1)
803 // is buffer aligned?
804 // process floats one-by-one if not
805 addloopchvol_unaligned
:
807 jz addloopchvol_aligned
;
808 // using `movsd` brings some penalty here, but meh...
809 movsd XMM3
,[EAX
]; // load two floats (single double), clear others
811 movsd [EAX
],XMM3
; // store two floats (single double)
814 jnz addloopchvol_unaligned
;
815 jmp addloopchvol_done
;
816 addloopchvol_aligned
:
834 if (ch
.lastvolL
!= 255 || ch
.lastvolR
!= 255) {
835 immutable float mulL
= (1.0f/255.0f)*cast(float)ch
.lastvolL
;
836 immutable float mulR
= (1.0f/255.0f)*cast(float)ch
.lastvolR
;
837 foreach (immutable _
; 0..fblen
) {
846 //{ import core.stdc.stdio; printf("have %u frames out of %u\n", ch.bufpos/2, bufsz/2); }
847 // if we have any data in channel buffer, resample it
848 uint blen
= void, bsused
= void;
850 auto xdd
= sndrsbufptr
+rspos
;
851 if (ch
.lastsrate
!= realSampleRate
) {
853 xss
= tmprsbufptr
+rspos
;
856 while (bsused
< ch
.bufpos
&& rspos
< bufsz
) {
857 srbdata
.dataIn
= ch
.bufptr
[bsused
..ch
.bufpos
];
858 srbdata
.dataOut
= tmprsbufptr
[rspos
..bufsz
];
859 //{ import core.stdc.stdio; printf("realSampleRate=%u; ch.lastsrate=%u\n", realSampleRate, ch.lastsrate); }
860 err
= (ch
.useCubic ? ch
.cub
.process(srbdata
) : ch
.srb
.process(srbdata
));
861 if (err
) { killChan(ch
, channelsChanged
); break; }
862 //{ import core.stdc.stdio; printf("inused: %u of %u; outused: %u of %u; bsused=%u\n", srbdata.inputSamplesUsed, cast(uint)srbdata.dataIn.length, srbdata.outputSamplesUsed, cast(uint)srbdata.dataOut.length, bsused); }
863 bsused
+= srbdata
.inputSamplesUsed
;
864 rspos
+= srbdata
.outputSamplesUsed
;
865 blen
+= srbdata
.outputSamplesUsed
;
868 // no need to resample, can use data as-is
870 bsused
= blen
= ch
.bufpos
;
873 // if we have something to mix...
875 // ...mix it; will clamp data later (this should theoretically improve mixing quality)
876 ch
.genFrames
+= blen
/2;
877 version(follin_use_sse
) {
878 asm nothrow @safe @nogc {
882 // dest may be unaligned, check it
884 jz diraddmix_daligned
;
885 // here we should process some floats to become aligned again
889 // now DL is the number of unaligned floats
890 jz diraddmix_daligned
;
891 sub ECX
,EDX
; // will check ECX later
892 // we can do it faster by unrolling here, but meh...
895 movss XMM2
,[EBX
]; // load single float, clear others
896 movss XMM3
,[EAX
]; // load single float, clear others
898 movss [EAX
],XMM3
; // store single float
903 jnz diraddmix_unaligned
;
904 // ECX is actually really small, so check the high bit to detect overflow
905 test ECX
,0x8000_0000U;
907 // has something more to do
910 mov EDX
,16; // memory increment
913 // is source aligned too?
916 // good case: source is aligned
928 // bad case: source is not aligned
943 foreach (immutable _
; 0..blen
) *xdd
++ += *xss
++;
947 // remove data from input buffer
948 if (bsused
>= ch
.bufpos
) {
950 } else if (bsused
> 0) {
951 import core
.stdc
.string
: memmove
;
952 memmove(ch
.bufptr
, ch
.bufptr
+bsused
, (ch
.bufpos
-bsused
)*ch
.bufptr
[0].sizeof
);
956 // if this channel is paused or dead, do not try to process it again
957 if (ch
.chan
is null || ch
.chan
.paused
) {
958 // try to squeeze last resampled data bytes out of it
959 if (rspos
< bufsz
&& ch
.lastsrate
!= realSampleRate
) {
960 // here we can shift tmprsbufptr to get both buffers aligned
961 blen
= bufsz
-rspos
; // we don't need more than this
962 xss
= tmprsbufptr
+(rspos
&0x03); // if rspos is not aligned, shift xss
963 srbdata
.dataIn
= null;
964 srbdata
.dataOut
= xss
[0..blen
];
965 err
= (ch
.useCubic ? ch
.cub
.process(srbdata
) : ch
.srb
.process(srbdata
));
966 if (err
) { killChan(ch
, channelsChanged
); break chmixloop
; }
967 if (ch
.lastquality
< 0 && ch
.bufpos
< bufsz
) ch
.cub
.reset(); // it is fast
968 if (srbdata
.outputSamplesUsed
) {
969 // ...mix it; will clamp data later (this should theoretically improve mixing quality)
970 xdd
= sndrsbufptr
+rspos
;
971 rspos
+= srbdata
.outputSamplesUsed
;
972 version(follin_use_sse
) {
973 blen
= srbdata
.outputSamplesUsed
;
974 asm nothrow @safe @nogc {
978 // dest may be unaligned, check it
980 jz diraddmix2_daligned
;
981 // here we should process some floats to become aligned again
985 // now DL is the number of unaligned floats
986 jz diraddmix2_daligned
;
987 sub ECX
,EDX
; // will check ECX later
988 // we can do it faster by unrolling here, but meh...
990 diraddmix2_unaligned
:
991 movss XMM2
,[EBX
]; // load single float, clear others
992 movss XMM3
,[EAX
]; // load single float, clear others
994 movss [EAX
],XMM3
; // store single float
999 jnz diraddmix2_unaligned
;
1000 // ECX is actually really small, so check the high bit to detect overflow
1001 test ECX
,0x8000_0000U;
1002 jnz diraddmix2_done
;
1003 // has something more to do
1004 diraddmix2_daligned
:
1006 mov EDX
,16; // memory increment
1009 // source is aligned too
1019 jnz diraddmix2_good
;
1023 foreach (immutable _
; 0..srbdata
.outputSamplesUsed
) *xdd
++ += *xss
++;
1030 // what is left is silence, no need to mix it
1031 wasAtLeastOne
= wasAtLeastOne ||
(rspos
> 0);
1034 if (channelsChanged
) packChannels();
1037 version(follin_use_spinrw
) {
1038 sndMutexChanRW
.writeLock();
1040 sndMutexChanRW
.writeUnlock();
1042 synchronized (sndMutexChanRW
.writer
) { mixin(doer
); }
1045 // do final converting
1046 auto dp
= sndbufptr
[buf2fill
];
1047 if (wasAtLeastOne
) {
1048 // something is playing, mix it
1049 auto mvol
= atomicLoad(sndMasterVolume
);
1051 version(follin_use_sse
) {
1052 asm nothrow @safe @nogc {
1053 mov ECX
,[sndSamplesSize
];
1054 // (sndSamplesSize+7)/8 -- destination is s16, not float
1057 mov EAX
,[dp
]; // dest, aligned
1058 //mov EBX,offsetof zeroes[0]; // lucky me, floating zero is s16 zero too
1059 mov EBX
,[zeroesptr
]; // lucky me, floating zero is s16 zero too
1060 //movntdqa XMM0,[EBX]; // non-temporal, don't bring zeroes to cache (sse4.1)
1071 dp
[0..sndSamplesSize
] = 0;
1074 auto mvolL
= mvol
&0xff;
1075 auto mvolR
= (mvol
>>8)&0xff;
1076 auto src
= sndrsbufptr
;
1077 version(follin_use_sse
) {
1078 //align(64) __gshared float[4] fmin4 = -32768.0;
1079 //align(64) __gshared float[4] fmax4 = 32767.0;
1080 align(64) __gshared
float[4] mvolf
= void;
1081 mvolf
[0] = mvolf
[2] = (32768.0/255.0f)*cast(float)mvolL
;
1082 mvolf
[1] = mvolf
[3] = (32768.0/255.0f)*cast(float)mvolR
;
1083 auto blen
= (sndSamplesSize
+3)/4;
1084 asm nothrow @safe @nogc {
1085 //mov EAX,offsetof fmin4[0];
1086 //movntdqa XMM2,[EAX]; // XMM2: min values
1087 //mov EAX,offsetof fmax4[0];
1088 //movntdqa XMM3,[EAX]; // XMM3: max values
1089 mov EAX
,offsetof mvolf
[0]; // source
1090 //movntdqa XMM4,[EAX]; // XMM4: multipliers (sse4.1)
1092 // source and dest are aligned
1093 mov EAX
,[src
]; // source
1094 mov EBX
,[dp
]; // dest
1100 mulps XMM0
,XMM4
; // mul by volume and shift
1101 //maxps XMM0,XMM2; // clip lower
1102 //minps XMM0,XMM3; // clip upper
1104 version(follin_use_sse2
) asm nothrow @safe @nogc {
1105 cvttps2dq XMM1
,XMM0
; // XMM1 now contains four int32 values
1108 } else asm nothrow @safe @nogc {
1109 cvtps2pi MM0
,XMM0
; // MM0 now contains two low int32 values
1110 movhlps XMM5
,XMM0
; // get high floats
1111 cvtps2pi MM1
,XMM5
; // MM1 now contains two high int32 values
1112 packssdw MM0
,MM1
; // MM0 now contains 4 int16 values
1115 asm nothrow @safe @nogc {
1121 version(follin_use_sse2
) {} else {
1122 asm nothrow @safe @nogc { emms; }
1125 immutable float mull
= (1.0f/255.0f)*mvolL
;
1126 immutable float mulr
= (1.0f/255.0f)*mvolR
;
1127 mixin(declfcvar
!"temp");
1128 foreach (immutable _
; 0..sndSamplesSize
/2) {
1130 float f
= (*src
++)*mull
;
1132 mixin(FAST_SCALED_FLOAT_TO_INT
!("f", "15"));
1133 if (cast(uint)(v
+32768) > 65535) v
= (v
< 0 ?
-32768 : 32767);
1134 *dp
++ = cast(short)v
;
1139 mixin(FAST_SCALED_FLOAT_TO_INT
!("f", "15"));
1140 if (cast(uint)(v
+32768) > 65535) v
= (v
< 0 ?
-32768 : 32767);
1141 *dp
++ = cast(short)v
;
1147 dp
[0..sndSamplesSize
] = 0;
1149 if (sndS16CB
!is null) sndS16CB(dp
[0..sndSamplesSize
]);
1150 atomicStore(sndbufToFill
, (buf2fill
+1)%bufcount
);
1151 atomicStore(sndbufFillingNow
, false);
1152 return wasAtLeastOne
;
1156 // ////////////////////////////////////////////////////////////////////////// //
1157 private extern (C
) void thread_suspendAll() nothrow; // steal that func!
1160 // ////////////////////////////////////////////////////////////////////////// //
1161 // mixer thread: it mixes sound buffers
1162 // as modern systems had multicore CPUs, i believe that it worth having two worker threads
1163 void sndMixTreadFunc () {
1164 // detach ourself, so GC will not stop us
1165 thread_detachThis();
1166 // while documentation says that we have to call this, in reality
1167 // i see one more module tls dtor called with it. this is due to
1168 // the fact that `thread_entryPoint()` will call `rt_moduleTlsDtor()`
1169 // on exiting. let's hope that modules won't rely on GC suspending
1170 // all threads on collecting (they shouldn't)
1171 //rt_moduleTlsDtor();
1173 version(follin_threads_debug
) { import core
.stdc
.stdio
; printf("mixer thread started\n"); }
1175 if (atomicLoad(sndWantShutdown
)) { atomicStore(sndSafeToShutdown0
, true); return; }
1176 // do we have a room for new buffer?
1177 auto b2f
= atomicLoad(sndbufToFill
);
1178 auto b2p
= atomicLoad(sndbufToPlay
);
1179 // note that if playback is paused, we will eventually fill all buffers and wait
1182 version(follin_threads_debug
) { import core
.stdc
.stdio
; printf("creating %u buffer; playing %u buffer\n", b2f
, b2p
); }
1183 sndGenerateBuffer();
1185 version(follin_threads_debug
) { import core
.stdc
.stdio
; printf("mixer thread fell asleep; 2play=%u; 2fill=%u\n", atomicLoad(sndbufToPlay
), atomicLoad(sndbufToFill
)); }
1186 synchronized(mutexCondMixer
) condMixerWait
.wait();
1187 version(follin_threads_debug
) { import core
.stdc
.stdio
; printf("mixer thread awoken; 2play=%u; 2fill=%u\n", atomicLoad(sndbufToPlay
), atomicLoad(sndbufToFill
)); }
1190 } catch (Throwable e
) {
1191 // here, we are dead and fucked (the exact order doesn't matter)
1192 import core
.stdc
.stdlib
: abort
;
1193 import core
.stdc
.stdio
: fprintf
, stderr
;
1194 import core
.memory
: GC
;
1195 GC
.disable(); // yeah
1196 thread_suspendAll(); // stop right here, you criminal scum!
1197 auto s
= e
.toString();
1198 fprintf(stderr
, "\n=== FATAL ===\n%.*s\n", cast(uint)s
.length
, s
.ptr
);
1199 abort(); // die, you bitch!
1204 // player thread: it plays mixed buffers
1205 void sndPlayTreadFunc () {
1206 // detach ourself, so GC will not stop us
1207 thread_detachThis(); // see comment in `sndMixTreadFunc()`
1210 foreach (uint bufnum
; 0..bufcount
) {
1211 atomicStore(sndbufToPlay
, bufnum
+1); // hoax
1212 atomicStore(sndbufToFill
, bufnum
);
1213 // generate one buffer
1214 sndGenerateBuffer();
1216 // restore playing and filling order
1217 atomicStore(sndbufToFill
, 0);
1218 atomicStore(sndbufToPlay
, 0);
1219 // and go with the sound
1220 version(follin_threads_debug
) { import core
.stdc
.stdio
; printf("starting playback\n"); }
1221 bool playbackStarted
= false;
1223 if (atomicLoad(sndWantShutdown
)) { atomicStore(sndSafeToShutdown1
, true); return; }
1224 auto b2p
= atomicLoad(sndbufToPlay
);
1225 auto consumed
= sndWriteBuffer(playbackStarted
);
1228 // fix channel playing time
1230 if (firstFreeChan
> 0) {
1231 foreach (immutable idx
; 0..chans
.length
) {
1232 auto ch
= chans
.ptr
+idx
;
1233 if (ch
.namelen
== 0) break; // last channel
1234 //{ import core.stdc.stdio; printf("pf: %u; gf: %u\n", cast(uint)ch.playedFrames, cast(uint)ch.genFrames); }
1235 if (ch
.playedFrames
< ch
.genFrames
) {
1236 ch
.playedFrames
+= sndSamplesSize
/2;
1237 if (ch
.playedFrames
> ch
.genFrames
) ch
.playedFrames
= ch
.genFrames
;
1242 version(follin_use_spinrw
) {
1243 sndMutexChanRW
.writeLock();
1245 sndMutexChanRW
.writeUnlock();
1247 synchronized (sndMutexChanRW
.writer
) { mixin(doer
); }
1249 b2p
= (b2p
+1)%bufcount
;
1250 atomicStore(sndbufToPlay
, b2p
);
1251 //if (b2p != atomicLoad(sndbufToFill)) atomicStore(sndbufToPlay, (b2p+1)%bufcount);
1252 version(follin_threads_debug
) { import core
.stdc
.stdio
; printf("pinging mixer thread; b2p=%u; b2f=%u\n", atomicLoad(sndbufToPlay
), atomicLoad(sndbufToFill
)); }
1253 synchronized(mutexCondMixer
) condMixerWait
.notify();
1256 } catch (Throwable e
) {
1257 // here, we are dead and fucked (the exact order doesn't matter)
1258 import core
.stdc
.stdlib
: abort
;
1259 import core
.stdc
.stdio
: fprintf
, stderr
;
1260 import core
.memory
: GC
;
1261 GC
.disable(); // yeah
1262 thread_suspendAll(); // stop right here, you criminal scum!
1263 auto s
= e
.toString();
1264 fprintf(stderr
, "\n=== FATAL ===\n%.*s\n", cast(uint)s
.length
, s
.ptr
);
1265 abort(); // die, you bitch!
1270 // ////////////////////////////////////////////////////////////////////////// //
1271 shared static ~this () nothrow @trusted /*@nogc*/ { sndEngineDeinit(); }
1274 void sndEngineDeinit () nothrow @trusted {
1275 //{ import core.stdc.stdio; printf("DEINIT...\n"); }
1276 if (cas(&sndWantShutdown
, false, true)) {
1277 //{ import core.stdc.stdio; printf("DEINIT: 0\n"); }
1278 if (atomicLoad(initialized
)) {
1279 import core
.sys
.posix
.unistd
: usleep
;
1280 while (!atomicLoad(sndSafeToShutdown0
)) { synchronized(mutexCondMixer
) condMixerWait
.notify(); usleep(1000); } // arbitrary number
1281 while (!atomicLoad(sndSafeToShutdown1
)) usleep(1000); // arbitrary number
1282 //{ import core.stdc.stdio; printf("DEINIT: 1\n"); }
1283 sndThread
= sndMixThread
= null;
1286 foreach (ref ch; chans) {
1287 import core.stdc.stdlib : free;
1290 if (ch.bufMem !is null) { free(ch.bufMem); ch.bufMem = null; }
1294 atomicStore(initialized
, false);
1297 atomicStore(sndWantShutdown
, false);
1298 atomicStore(sndSafeToShutdown0
, false);
1299 atomicStore(sndSafeToShutdown1
, false);