1 // sample OpenAL streaming player
2 // based on the code by David Gow <david@ingeniumdigital.com>
4 module openal_streaming
is aliced
;
14 enum BufferSizeBytes
= 960*2*2;
16 // the number of buffers we'll be rotating through
17 // ideally, all but one will be full
25 ulong framesDone
; // with buffer precision
30 void warn (string w
, uint durms
=1500) {
33 warnStartFrm
= framesDone
;
41 // returns number of *samples* (not frames!) queued, -1 on error, 0 on EOF
42 int fillBuffer (ref AudioStream ass
, ALuint buffer
) {
43 // let's have a buffer that is two opus frames long (and two channels)
44 short[BufferSizeBytes
] buf
= void; // no need to initialize it
46 immutable int numChannels
= ass
.channels
;
48 // we only support stereo and mono
49 if (numChannels
< 1 || numChannels
> 2) {
50 stderr
.writeln("File contained more channels than we support (", numChannels
, ").");
55 // keep reading samples until we have them all
56 while (samplesRead
< BufferSizeBytes
) {
57 int ns
= ass
.readFrames(buf
.ptr
+samplesRead
, (BufferSizeBytes
-samplesRead
)/numChannels
);
58 if (ns
< 0) { stderr
.writeln("ERROR reading audio file!"); return -1; }
60 samplesRead
+= ns
*numChannels
;
63 if (samplesRead
> 0) {
65 // try to use OpenAL Soft extension first
66 static if (AL_SOFT_buffer_samples
) {
68 static bssChecked
= -1;
70 if (alIsExtensionPresent("AL_SOFT_buffer_samples")) {
71 if (alGetProcAddress("alIsBufferFormatSupportedSOFT") !is null &&
72 alGetProcAddress("alBufferSamplesSOFT") !is null) bssChecked
= 1; else bssChecked
= 0;
73 //writeln("bssChecked=", bssChecked);
77 if (!bssChecked
) writeln("OpenAL: no 'AL_SOFT_buffer_samples'");
80 static bool warningDisplayed
= false;
81 final switch (numChannels
) {
82 case 1: format
= AL_MONO16_SOFT
; chantype
= AL_MONO_SOFT
; break;
83 case 2: format
= AL_STEREO16_SOFT
; chantype
= AL_STEREO_SOFT
; break;
85 if (alIsBufferFormatSupportedSOFT(format
)) {
86 alBufferSamplesSOFT(buffer
, ass
.rate
, format
, samplesRead
/numChannels
, chantype
, AL_SHORT_SOFT
, buf
.ptr
);
89 if (!warningDisplayed
) { warningDisplayed
= true; stderr
.writeln("fallback!"); }
92 // use normal OpenAL method
93 final switch (numChannels
) {
94 case 1: format
= AL_FORMAT_MONO16
; break;
95 case 2: format
= AL_FORMAT_STEREO16
; break;
97 alBufferData(buffer
, format
, buf
.ptr
, samplesRead
*2, ass
.rate
);
104 bool updateStream (ref AudioStream ass
, ALuint source
, ref PlayTime ptime
) {
105 //bool someBufsAdded = false;
106 ALuint currentbuffer
;
108 // how many buffers do we need to fill?
109 int numProcessedBuffers
= 0;
110 alGetSourcei(source
, AL_BUFFERS_PROCESSED
, &numProcessedBuffers
);
112 if (numProcessedBuffers
> 0) {
113 // unqueue a finished buffer, fill it with new data, and re-add it to the end of the queue
114 while (numProcessedBuffers
--) {
115 alSourceUnqueueBuffers(source
, 1, ¤tbuffer
);
116 // add number of played samples to playtime
118 alGetBufferi(currentbuffer
, AL_SIZE
, &bufsz
);
119 ptime
.framesDone
+= bufsz
/2/ass
.channels
;
120 //writeln("buffer size: ", bufsz);
121 if (ass
.fillBuffer(currentbuffer
) <= 0) return false;
122 //someBufsAdded = true;
123 alSourceQueueBuffers(source
, 1, ¤tbuffer
);
131 // AudioStream is required to get sampling rate and number of channels
132 uint getPositionMSec (ref AudioStream ass
, ALuint source
, in ref PlayTime ptime
) {
133 ulong frames
= ptime
.framesDone
;
135 alGetSourcei(source
, AL_SAMPLE_OFFSET
, &offset
); // in the current buffer
136 if (alGetError() == AL_NO_ERROR
) {
137 // add processed buffers (assume that all buffers are of the same size)
138 int numProcessedBuffers
= 0;
139 alGetSourcei(source
, AL_BUFFERS_PROCESSED
, &numProcessedBuffers
);
140 if (alGetError() == AL_NO_ERROR
) {
141 frames
+= numProcessedBuffers
*(BufferSizeBytes
/2/ass
.channels
);
142 frames
+= offset
/ass
.channels
;
149 return cast(uint)(frames
*1000/ass
.rate
);
153 // load an ogg opus file into the given AL buffer
154 void streamAudioFile (ALuint source
, string filename
) {
158 writeln("opening '", filename
, "'...");
159 auto ass
= AudioStream
.detect(VFile(filename
));
160 scope(exit
) ass
.close();
162 // get the number of channels in the current link
163 immutable int numChannels
= ass
.channels
;
164 // get the number of samples (per channel) in the current link
165 immutable long frameCount
= ass
.framesTotal
;
167 uint nextProgressTime
= 0;
173 static if (AL_SOFT_source_latency) {
174 if (alIsExtensionPresent("AL_SOFT_source_latency")) {
175 ALint64SOFT[2] smpvals;
176 ALdouble[2] timevals;
177 alGetSourcei64vSOFT(source, AL_SAMPLE_OFFSET_LATENCY_SOFT, smpvals.ptr);
178 alGetSourcedvSOFT(source, AL_SEC_OFFSET_LATENCY_SOFT, timevals.ptr);
179 writeln("sample: ", smpvals[0]>>32, "; latency (ns): ", smpvals[1], "; seconds=", timevals[0], "; latency (msecs)=", timevals[1]*1000);
185 alGetSourcedSOFT(source
, AL_SEC_LENGTH_SOFT
, &blen
);
186 writeln("slen: ", blen
);
190 if (ptime
.newWarning
&& ptime
.warning
.length
== 0) ptime
.newWarning
= false;
192 if (ptime
.newWarning
) {
193 import std
.string
: toStringz
;
194 import core
.stdc
.stdio
: stdout
, fprintf
;
195 if (ptime
.warnWasPainted
) stdout
.fprintf("\e[A");
196 stdout
.fprintf("\r%s\e[K\n", ptime
.warning
.toStringz
);
197 ptime
.warnWasPainted
= true;
198 ptime
.newWarning
= false;
199 nextProgressTime
= 0; // redraw time
202 uint time
= cast(uint)(ptime
.framesDone
*1000/ass
.rate
);
203 uint total
= cast(uint)(frameCount
*1000/ass
.rate
);
204 uint xtime
= getPositionMSec(ass
, source
, ptime
);
206 if (ptime
.warning
.length
> 0 && ptime
.warnWasPainted
) {
207 import core
.stdc
.stdio
: stdout
, fprintf
;
208 uint etime
= cast(uint)(ptime
.warnStartFrm
*1000/ass
.rate
)+ptime
.warnDurMsecs
;
210 stdout
.fprintf("\e\r[A\e[K\n");
211 ptime
.warning
= null;
212 nextProgressTime
= 0; // redraw time
216 if (time
>= nextProgressTime
) {
217 import core
.stdc
.stdio
: stdout
, fprintf
, fflush
;
219 stdout
.fprintf("\r%2u:%02u / %u:%02u (%u of %u) (%u : %u)\e[K", time
/60/1000, time
%60000/1000, total
/60/1000, total
%60000/1000, cast(uint)procBufs
, BufferCount
, time
, xtime
);
221 stdout
.fprintf("\r%2u:%02u / %u:%02u (%u : %u)\e[K", time
/60/1000, time
%60000/1000, total
/60/1000, total
%60000/1000, time
, xtime
);
224 nextProgressTime
= time
+500;
229 nextProgressTime
= 0;
231 import core
.stdc
.stdio
: stdout
, fprintf
, fflush
;
232 stdout
.fprintf("\n");
237 writeln(filename
, ": ", numChannels
, " channels, ", frameCount
, " frames (", ass
.timeTotal
/1000, " seconds)");
239 ALuint
[BufferCount
] buffers
; // no need to initialize it, but why not?
241 alGenBuffers(BufferCount
, buffers
.ptr
);
243 foreach (ref buf
; buffers
) ass
.fillBuffer(buf
); //TODO: check for errors here too
245 alSourceQueueBuffers(source
, BufferCount
, buffers
.ptr
);
247 ulong stt
= clockMicro();
249 alSourcePlay(source
);
250 if (alGetError() != AL_NO_ERROR
) throw new Exception("Could not play source!");
254 import core
.sys
.posix
.unistd
: usleep
;
255 //usleep(sleepTimeNS);
257 // sleep until at least one buffer is empty
258 ulong ett
= stt
+(ass
.rate
*1000/(BufferSizeBytes
/2/numChannels
));
259 ulong ctt
= clockMicro();
260 //writeln(" ", ctt, " ", ett, " " , ett-ctt);
261 if (ctt
< ett
&& ett
-ctt
> 100) usleep(cast(uint)(ett
-ctt
)-100);
263 alGetSourcei(source
, AL_BUFFERS_PROCESSED
, &procBufs
);
265 if (!ass
.updateStream(source
, ptime
)) break pumploop
;
267 // source can stop playing on buffer underflow
270 alGetSourcei(source
, AL_SOURCE_STATE
, &sourceState
);
271 if (sourceState
!= AL_PLAYING
&& sourceState
!= AL_PAUSED
) {
273 int numProcessedBuffers
= 0;
274 alGetSourcei(source
, AL_BUFFERS_PROCESSED
, &numProcessedBuffers
);
275 writeln(" npb=", numProcessedBuffers
, " of ", BufferCount
);
277 ptime
.warn("Source not playing!", 600);
278 alSourcePlay(source
);
282 // actually, "waiting" should go into time display too
285 // wait for source to finish playing
286 writeln("waiting source to finish playing...");
289 alGetSourcei(source
, AL_SOURCE_STATE
, &sourceState
);
290 if (sourceState
!= AL_PLAYING
) break;
293 alSourceUnqueueBuffers(source
, BufferCount
, buffers
.ptr
);
295 // we have to delete the source here, as OpenAL soft seems to need the source gone before the buffers
296 // perhaps this is just timing
297 alDeleteSources(1, &source
);
298 alDeleteBuffers(BufferCount
, buffers
.ptr
);
302 void main (string
[] args
) {
303 import std
.string
: fromStringz
;
306 ALfloat listenerGain
= 1.0f;
307 bool limiting
= true;
312 auto gof
= getopt(args
,
313 std
.getopt
.config
.caseSensitive
,
314 std
.getopt
.config
.bundling
,
315 "gain|g", &listenerGain
,
316 "limit|l", &limiting
,
319 if (args
.length
<= 1) throw new Exception("filename?!");
321 writeln("OpenAL device extensions: ", alcGetString(null, ALC_EXTENSIONS
).fromStringz
);
323 if (alcIsExtensionPresent(null, "ALC_ENUMERATE_ALL_EXT")) {
324 auto hwdevlist
= alcGetString(null, ALC_ALL_DEVICES_SPECIFIER
);
325 writeln("OpenAL hw devices:");
327 writeln(" ", hwdevlist
.fromStringz
);
328 while (*hwdevlist
) ++hwdevlist
;
333 if (alcIsExtensionPresent(null, "ALC_ENUMERATION_EXT")) {
334 auto devlist
= alcGetString(null, ALC_DEVICE_SPECIFIER
);
335 writeln("OpenAL renderers:");
337 writeln(" ", devlist
.fromStringz
);
338 while (*devlist
) ++devlist
;
343 writeln("OpenAL default renderer: ", alcGetString(null, ALC_DEFAULT_DEVICE_SPECIFIER
).fromStringz
);
345 // open the default device
346 dev
= alcOpenDevice(null);
347 if (dev
is null) throw new Exception("couldn't open OpenAL device");
348 scope(exit
) alcCloseDevice(dev
);
350 writeln("OpenAL renderer: ", alcGetString(dev
, ALC_DEVICE_SPECIFIER
).fromStringz
);
351 writeln("OpenAL hw device: ", alcGetString(dev
, ALC_ALL_DEVICES_SPECIFIER
).fromStringz
);
353 // we want an OpenAL context
355 immutable ALCint
[$] attrs
= [
356 ALC_STEREO_SOURCES
, 1, // get at least one stereo source for music
357 ALC_MONO_SOURCES
, 1, // this should be audio channels in our game engine
358 //ALC_FREQUENCY, 48000, // desired frequency; we don't really need this, let OpenAL choose the best
361 ctx
= alcCreateContext(dev
, attrs
.ptr
);
363 if (ctx
is null) throw new Exception("couldn't create OpenAL context");
365 // just to show you how it's done
366 if (alcIsExtensionPresent(null, "ALC_EXT_thread_local_context")) alcSetThreadContext(null); else alcMakeContextCurrent(null);
367 alcDestroyContext(ctx
);
370 if (!limiting
&& alcGetProcAddress(dev
, "alcResetDeviceSOFT") !is null) {
371 immutable ALCint
[$] attrs
= [
372 ALC_OUTPUT_LIMITER_SOFT
, ALC_FALSE
,
375 if (!alcResetDeviceSOFT(dev
, attrs
.ptr
)) stderr
.writeln("WARNING: can't turn off OpenAL limiter");
378 // just to show you how it's done; see https://github.com/openalext/openalext/blob/master/ALC_EXT_thread_local_context.txt
379 if (alcIsExtensionPresent(null, "ALC_EXT_thread_local_context")) alcSetThreadContext(ctx
); else alcMakeContextCurrent(ctx
);
380 //alcMakeContextCurrent(ctx);
382 writeln("OpenAL vendor: ", alGetString(AL_VENDOR
).fromStringz
);
383 writeln("OpenAL version: ", alGetString(AL_VERSION
).fromStringz
);
384 writeln("OpenAL renderer: ", alGetString(AL_RENDERER
).fromStringz
);
385 writeln("OpenAL extensions: ", alGetString(AL_EXTENSIONS
).fromStringz
);
387 // get us a buffer and a source to attach it to
388 writeln("creating OpenAL source...");
389 alGenSources(1, &testSource
);
391 // this turns off OpenAL spatial processing for the source,
392 // thus directly mapping stereo sound to the corresponding channels;
393 // but this works only for stereo samples, and we'd better do that
394 // after checking number of channels in input stream
395 static if (AL_SOFT_direct_channels
) {
396 if (alIsExtensionPresent("AL_SOFT_direct_channels")) {
397 writeln("OpenAL: direct channels extension detected");
398 alSourcei(testSource
, AL_DIRECT_CHANNELS_SOFT
, AL_TRUE
);
399 if (alGetError() != AL_NO_ERROR
) stderr
.writeln("WARNING: can't turn on direct channels");
403 writeln("setting OpenAL listener...");
404 // set position and gain for the listener
405 alListener3f(AL_POSITION
, 0.0f, 0.0f, 0.0f);
406 //alListenerf(AL_GAIN, 1.0f);
407 // as listener gain is applied after source gain, and it not limited to 1.0, it is possible to do the following
408 writeln("listener gain: ", listenerGain
);
409 alListenerf(AL_GAIN
, listenerGain
);
411 // ...and set source properties
412 writeln("setting OpenAL source properties...");
413 alSource3f(testSource
, AL_POSITION
, 0.0f, 0.0f, 0.0f);
417 //alGetSourcef(testSource, AL_MAX_GAIN, &maxGain);
418 //writeln("max gain: ", maxGain);
420 if (alIsExtensionPresent("AL_SOFT_gain_clamp_ex")) {
421 ALfloat gainLimit
= 0.0f;
422 alGetFloatv(AL_GAIN_LIMIT_SOFT
, &gainLimit
);
423 writeln("gain limit: ", gainLimit
);
426 alSourcef(testSource
, AL_GAIN
, 1.0f);
427 // MAX_GAIN is *user* limit, not library/hw; so you can do the following
428 // but somehow it doesn't work right on my system (or i misunderstood it's use case)
429 // it seems to slowly fall back to 1.0, and distort both volume and (sometimes) pitch
431 alSourcef(testSource
, AL_MAX_GAIN
, 2.0f);
432 alSourcef(testSource
, AL_GAIN
, 2.0f);
435 if (alGetError() != AL_NO_ERROR
) throw new Exception("error initializing OpenAL");
437 writeln("streaming...");
438 streamAudioFile(testSource
, args
[1]);